Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.06% covered (danger)
47.06%
1050 / 2231
54.81% covered (warning)
54.81%
57 / 104
CRAP
0.00% covered (danger)
0.00%
0 / 1
SeedDMS_Core_DMS
46.75% covered (danger)
46.75%
1037 / 2218
54.81% covered (warning)
54.81%
57 / 104
152871.23
0.00% covered (danger)
0.00%
0 / 1
 checkIfEqual
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 inList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 checkDate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 filterAccess
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 filterUsersByAccess
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 filterDocumentLinks
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
90
 mergeAccessLists
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
306
 filterDocumentFiles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
56
 __construct
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
4.00
 getClassname
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setClassname
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getDecorators
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addDecorator
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDB
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStorage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDBVersion
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 checkVersion
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
7
 setRootFolderID
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 setMaxDirID
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRootFolder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setForceRename
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setForceLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getLoggedInUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocument
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsLockedByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDocumentsExpired
64.71% covered (warning)
64.71%
44 / 68
0.00% covered (danger)
0.00%
0 / 1
59.05
 getDocumentByName
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 getDocumentByOriginalFilename
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
7
 getDocumentContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 countTasks
0.00% covered (danger)
0.00%
0 / 119
0.00% covered (danger)
0.00%
0 / 1
1122
 getDocumentList
13.30% covered (danger)
13.30%
79 / 594
0.00% covered (danger)
0.00%
0 / 1
37779.18
 makeTimeStamp
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
23
 search
47.47% covered (danger)
47.47%
197 / 415
0.00% covered (danger)
0.00%
0 / 1
6540.66
 getFolder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFolderByName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 checkFolders
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
13
 checkDocuments
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
16
 getUser
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getUserByLogin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getUserByEmail
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllUsers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addUser
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
12
 getGroup
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 getGroupByName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getAllGroups
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addGroup
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 getRole
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRoleByName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAllRoles
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addRole
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getTransmittal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTransmittalByName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAllTransmittals
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addTransmittal
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getKeywordCategory
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getKeywordCategoryByName
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getAllKeywordCategories
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 getAllUserKeywordCategories
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 addKeywordCategory
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 getDocumentCategory
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getDocumentCategories
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getDocumentCategoryByName
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 addDocumentCategory
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getNotificationsByGroup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNotificationsByUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createPasswordRequest
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 checkPasswordRequest
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 deletePasswordRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAttributeDefinition
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getAttributeDefinitionByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllAttributeDefinitions
82.61% covered (warning)
82.61%
19 / 23
0.00% covered (danger)
0.00%
0 / 1
12.76
 addAttributeDefinition
94.44% covered (success)
94.44%
17 / 18
0.00% covered (danger)
0.00%
0 / 1
8.01
 getAllWorkflows
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
 getWorkflow
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 addWorkflow
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowState
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowStateByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllWorkflowStates
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addWorkflowState
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowAction
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 getWorkflowActionByName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllWorkflowActions
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 addWorkflowAction
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 getWorkflowTransition
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getDocumentsInReception
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getDocumentsInRevision
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getUnlinkedDocumentContent
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
3.85
 getNoFileSizeDocumentContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getNoChecksumDocumentContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 getWrongFiletypeDocumentContent
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
156
 getDuplicateDocumentContent
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 getDuplicateSequenceNo
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getLinksToItself
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getProcessWithoutUserGroup
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
56
 removeProcessWithoutUserGroup
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 getStatisticalData
70.89% covered (warning)
70.89%
56 / 79
0.00% covered (danger)
0.00%
0 / 1
76.53
 getTimeline
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
6.47
 getLatestChanges
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
380
 setCallback
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 addCallback
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 hasCallback
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2declare(strict_types=1);
3
4/**
5 * Implementation of the document management system
6 *
7 * @category   DMS
8 * @package    SeedDMS_Core
9 * @license    GPL 2
10 * @author     Uwe Steinmann <uwe@steinmann.cx>
11 * @copyright  Copyright (C) 2010-2024 Uwe Steinmann
12 */
13
14/**
15 * Include some files
16 */
17require_once("inc.AccessUtils.php");
18require_once("inc.FileUtils.php");
19require_once("inc.ClassAccess.php");
20require_once("inc.ClassObject.php");
21require_once("inc.ClassFolder.php");
22require_once("inc.ClassDocument.php");
23require_once("inc.ClassGroup.php");
24require_once("inc.ClassUser.php");
25require_once("inc.ClassKeywords.php");
26require_once("inc.ClassNotification.php");
27require_once("inc.ClassAttribute.php");
28require_once("inc.ClassStorage.php");
29require_once("inc.ClassStorageFile.php");
30
31/**
32 * Class to represent the complete document management system.
33 * This class is needed to do most of the dms operations. It needs
34 * an instance of {@see SeedDMS_Core_DatabaseAccess} to access the
35 * underlying database. Many methods are factory functions which create
36 * objects representing the entities in the dms, like folders, documents,
37 * users, or groups.
38 *
39 * Each dms has its own database for meta data and a data store for document
40 * content. Both must be specified when creating a new instance of this class.
41 * All folders and documents are organized in a hierachy like
42 * a regular file system starting with a {@see SeedDMS_Core_DMS::rootFolderID}
43 *
44 * This class does not enforce any access rights on documents and folders
45 * by design. It is up to the calling application to use the methods
46 * {@see SeedDMS_Core_Folder::getAccessMode()} and
47 * {@see SeedDMS_Core_Document::getAccessMode()} and interpret them as desired.
48 * Though, there are two convenient functions to filter a list of
49 * documents/folders for which users have access rights for. See
50 * {@see SeedDMS_Core_DMS::filterAccess()}
51 * and {@see SeedDMS_Core_DMS::filterUsersByAccess()}
52 *
53 * Though, this class has a method to set the currently logged in user
54 * ({@see SeedDMS_Core_DMS::setUser()}), it does not have to be called, because
55 * there is currently no class within the SeedDMS core which needs the logged
56 * in user. {@see SeedDMS_Core_DMS} itself does not do any user authentication.
57 * It is up to the application using this class.
58 *
59 * ```php
60 * <?php
61 * include("inc/inc.ClassDMS.php");
62 * $db = new SeedDMS_Core_DatabaseAccess($type, $hostname, $user, $passwd, $name);
63 * $db->connect() or die ("Could not connect to db-server");
64 * $dms = new SeedDMS_Core_DMS($db, $contentDir);
65 * $dms->setRootFolderID(1);
66 * ...
67 * ?>
68 * ```
69 *
70 * @category   DMS
71 * @package    SeedDMS_Core
72 * @author     Uwe Steinmann <uwe@steinmann.cx>
73 * @copyright  Copyright (C) 2010-2024 Uwe Steinmann
74 */
75class SeedDMS_Core_DMS {
76    /**
77     * @var SeedDMS_Core_DatabaseAccess $db reference to database object. This must be an instance
78     *      of {@see SeedDMS_Core_DatabaseAccess}.
79     * @access protected
80     */
81    protected $db;
82
83    /**
84     * @var SeedDMS_Core_Storage $storage reference to storage object.
85     * This must be an instance {@see SeedDMS_Core_Storage_File}.
86     * @access protected
87     */
88    protected $storage;
89
90    /**
91     * @var array $classnames list of classnames for objects being instanciate
92     *      by the dms
93     * @access protected
94     */
95    protected $classnames;
96
97    /**
98     * @var array $decorators list of decorators for objects being instanciate
99     *      by the dms
100     * @access protected
101     */
102    protected $decorators;
103
104    /**
105     * @var SeedDMS_Core_User $user reference to currently logged in user. This must be
106     *      an instance of {@see SeedDMS_Core_User}. This variable is currently not
107     *      used. It is set by {@see SeedDMS_Core_DMS::setUser()}.
108     * @access private
109     */
110    private $user;
111
112    /**
113     * @var string $contentDir location in the file system where all the
114     *      document data is located. This should be an absolute path.
115     * @access public
116     */
117    public $contentDir;
118
119    /**
120     * @var integer $rootFolderID ID of root folder
121     * @access public
122     */
123    public $rootFolderID;
124
125    /**
126     * @var integer $maxDirID maximum number of documents per folder on the
127     *      filesystem. If this variable is set to a value != 0, the content
128     *      directory will have a two level hierarchy for document storage.
129     * @access public
130     */
131    public $maxDirID;
132
133    /**
134     * @var boolean $forceRename use renameFile() instead of copyFile() when
135     *      copying the document content into the data store. The default is
136     *      to copy the file. This parameter only affects the methods
137     *      SeedDMS_Core_Document::addDocument() and
138     *      SeedDMS_Core_Document::addDocumentFile(). Setting this to true
139     *      may save resources especially for large files.
140     * @access public
141     */
142    public $forceRename;
143
144    /**
145     * @var boolean $forceLink use linkFile() instead of copyFile() when
146     *      copying the document content into the data store. The default is
147     *      to copy the file. This parameter only affects the method
148     *      SeedDMS_Core_Document::addDocument(). Use this with care,
149     *      because it will leave the original document at its place.
150     * @access public
151     */
152    public $forceLink;
153
154    /**
155     * @var array $noReadForStatus list of status without read right
156     *      online. DO NOT USE ANYMORE. SeedDMS_Core_DocumentContent::getAccessMode()
157     *      was the only method using it, but it now takes the noReadForStatus info
158     *      from the user's role
159     * @access public
160     */
161    public $noReadForStatus;
162
163    /**
164     * @var boolean $checkWithinRootDir check if folder/document being accessed
165     *      is within the rootdir
166     * @access public
167     */
168    public $checkWithinRootDir;
169
170    /**
171     * @var string $version version of pear package
172     * @access public
173     */
174    public $version;
175
176    /**
177     * @var boolean $usecache true if internal cache shall be used
178     * @access public
179     */
180    public $usecache;
181
182    /**
183     * @var array $cache cache for various objects
184     * @access public
185     */
186    protected $cache;
187
188    /**
189     * @var array $callbacks list of methods called when certain operations,
190     * like removing a document, are executed. Set a callback with
191     * {@see SeedDMS_Core_DMS::setCallback()}.
192     * The key of the array is the internal callback function name. Each
193     * array element is an array with two elements: the function name
194     * and the parameter passed to the function.
195     *
196     * Currently implemented callbacks are:
197     *
198     * onPreRemoveDocument($user_param, $document);
199     *   called before deleting a document. If this function returns false
200     *   the document will not be deleted.
201     *
202     * onPostRemoveDocument($user_param, $document_id);
203     *   called after the successful deletion of a document.
204     *
205     * @access public
206     */
207    public $callbacks;
208
209    /**
210     * @var string last error message. This can be set by hooks to pass an
211     * error message from the hook to the application which has called the
212     * method containing the hook. For example SeedDMS_Core_Document::remove()
213     * calls the hook 'onPreRemoveDocument'. The hook function can set $dms->lasterror
214     * which can than be read when SeedDMS_Core_Document::remove() fails.
215     * This variable could be set in any SeedDMS_Core class, but is currently
216     * only set by hooks.
217     * @access public
218     */
219    public $lasterror;
220
221    /**
222     * @var SeedDMS_Core_DMS
223     */
224//    public $_dms;
225
226
227    /**
228     * Checks if two objects are equal by comparing their IDs
229     *
230     * The regular php check done by '==' compares all attributes of
231     * two objects, which is often not required. This method will first check
232     * if the objects are instances of the same class and than if they
233     * have the same id.
234     *
235     * @param object $object1 first object to be compared
236     * @param object $object2 second object to be compared
237     * @return boolean true if objects are equal, otherwise false
238     */
239    static function checkIfEqual($object1, $object2) { /* {{{ */
240        if(get_class($object1) != get_class($object2))
241            return false;
242        if($object1->getID() != $object2->getID())
243            return false;
244        return true;
245    } /* }}} */
246
247    /**
248     * Checks if a list of objects contains a single object by comparing their IDs
249     *
250     * This method is only applicable on list containing objects which have
251     * a method getID() because it is used to check if two objects are equal.
252     * The regular php check on objects done by '==' compares all attributes of
253     * two objects, which often isn't required. The method will first check
254     * if the objects are instances of the same class.
255     *
256     * The result of the function can be 0 which happens if the first element
257     * of an indexed array matches.
258     *
259     * @param object $object object to look for (needle)
260     * @param array $list list of objects (haystack)
261     * @return boolean|integer index in array if object was found, otherwise false
262     */
263    static function inList($object, $list) { /* {{{ */
264        foreach($list as $i=>$item) {
265            if(get_class($item) == get_class($object) && $item->getID() == $object->getID())
266                return $i;
267        }
268        return false;
269    } /* }}} */
270
271    /**
272     * Checks if date conforms to a given format
273     *
274     * @param string $date date to be checked
275     * @param string $format format of date. Will default to 'Y-m-d H:i:s' if
276     * format is not given.
277     * @return boolean true if date is in propper format, otherwise false
278     */
279    static function checkDate($date, $format='Y-m-d H:i:s') { /* {{{ */
280        $d = DateTime::createFromFormat($format, $date);
281        return $d && $d->format($format) == $date;
282    } /* }}} */
283
284    /**
285     * Filter out objects which are not accessible in a given mode by a user.
286     *
287     * The list of objects to be checked can be of any class, but has to have
288     * a method getAccessMode($user) which checks if the given user has at
289     * least the access right on the object as passed in $minMode.
290     * Hence, passing a group instead of a user is possible.
291     *
292     * This function can be used for documents and folders and calls
293     * {@link SeedDMS_Core_Folder::getAccessMode()} or
294     * {@link SeedDMS_Core_Document::getAccessMode()}. A document is also
295     * filtered out if it has no latest content, which can happen if access
296     * on documents in a certain state has been restricted.
297     *
298     * @param array $objArr list of objects (either documents or folders)
299     * @param object $user user for which access is checked
300     * @param integer $minMode minimum access mode required (M_ANY, M_NONE,
301     *        M_READ, M_READWRITE, M_ALL)
302     * @return array filtered list of objects
303     */
304    static function filterAccess($objArr, $user, $minMode) { /* {{{ */
305        if (!is_array($objArr)) {
306            return array();
307        }
308        $newArr = array();
309        foreach ($objArr as $obj) {
310            if ($obj->getAccessMode($user) >= $minMode) {
311                $dms = $obj->getDMS();
312                if($obj->isType('document')) {
313                    if($obj->getLatestContent())
314                        array_push($newArr, $obj);
315                } else {
316                    array_push($newArr, $obj);
317                }
318            }
319        }
320        return $newArr;
321    } /* }}} */
322
323    /**
324     * Filter out users which cannot access an object in a given mode.
325     *
326     * The list of users to be checked can be of any class, but has to have
327     * a method getAccessMode($user) which checks if a user has at least the
328     * access right as passed in $minMode. Hence, passing a list of groups
329     * instead of users is possible.
330     *
331     * @param object $obj object that shall be accessed
332     * @param array $users list of users/groups which are to check for sufficient
333     *        access rights
334     * @param integer $minMode minimum access right on the object for each user
335     *        (M_ANY, M_NONE, M_READ, M_READWRITE, M_ALL)
336     * @return array filtered list of users
337     */
338    static function filterUsersByAccess($obj, $users, $minMode) { /* {{{ */
339        $newArr = array();
340        foreach ($users as $currUser) {
341            if ($obj->getAccessMode($currUser) >= $minMode)
342                array_push($newArr, $currUser);
343        }
344        return $newArr;
345    } /* }}} */
346
347    /**
348     * Filter out document links which can not be accessed by a given user
349     *
350     * Returns a filtered list of links which are accessible by the
351     * given user. A link is only accessible, if it is publically visible,
352     * owned by the user, or the accessing user is an administrator.
353     *
354     * @param SeedDMS_Core_DocumentLink[] $links list of objects of type SeedDMS_Core_DocumentLink
355     * @param object $user user for which access is being checked
356     * @param string $access set if source or target of link shall be checked
357     * for sufficient access rights. Set to 'source' if the source document
358     * of a link is to be checked, set to 'target' for the target document.
359     * If not set, then access rights will not be checked at all.
360     * @return array filtered list of links
361     */
362    static function filterDocumentLinks($user, $links, $access='') { /* {{{ */
363        $tmp = array();
364        foreach ($links as $link) {
365            if ($link->isPublic() || ($link->getUser()->getID() == $user->getID()) || $user->isAdmin()){
366                if($access == 'source') {
367                    $obj = $link->getDocument();
368                    if ($obj->getAccessMode($user) >= M_READ)
369                        array_push($tmp, $link);
370                } elseif($access == 'target') {
371                    $obj = $link->getTarget();
372                    if ($obj->getAccessMode($user) >= M_READ)
373                        array_push($tmp, $link);
374                } else {
375                    array_push($tmp, $link);
376                }
377            }
378        }
379        return $tmp;
380    } /* }}} */
381
382    /**
383     * Merge access lists
384     *
385     * Merges two access lists. Objects of the second list will override objects
386     * in the first list.
387     *
388     * @param array $first list of access rights as returned by
389     * SeedDMS_Core_Document:: getAccessList() or SeedDMS_Core_Folder::getAccessList()
390     * @param array $secont list of access rights
391     * @return array merged list
392     */
393    static function mergeAccessLists($first, $second) { /* {{{ */
394        if($first && !$second)
395            return $first;
396        if(!$first && $second)
397            return $second;
398
399        $tmp = array('users'=>array(), 'groups'=>array());
400        if(!isset($first['users']) || !isset($first['groups']) ||
401            !isset($second['users']) || !isset($second['groups']))
402            return false;
403
404        foreach ($first['users'] as $f) {
405            $new = $f;
406            foreach ($second['users'] as $i=>$s) {
407                if($f->getUserID() == $s->getUserID()) {
408                    $new = $s;
409                    unset($second['users'][$i]);
410                    break;
411                }
412            }
413            array_push($tmp['users'], $new);
414        }
415        foreach ($seconf['users'] as $f) {
416            array_push($tmp['users'], $f);
417        }
418
419        foreach ($first['groups'] as $f) {
420            $new = $f;
421            foreach ($second['groups'] as $i=>$s) {
422                if($f->getGroupID() == $s->getGroupID()) {
423                    $new = $s;
424                    unset($second['groups'][$i]);
425                    break;
426                }
427            }
428            array_push($tmp['groups'], $new);
429        }
430        foreach ($second['groups'] as $f) {
431            array_push($tmp['groups'], $f);
432        }
433
434        return $tmp;
435    } /* }}} */
436
437    /*
438     * Filter out document attachments which can not be accessed by a given user
439     *
440     * Returns a filtered list of files which are accessible by the
441     * given user. A file is only accessible, if it is publically visible,
442     * owned by the user, or the accessing user is an administrator.
443     *
444     * @param array $files list of objects of type SeedDMS_Core_DocumentFile
445     * @param object $user user for which access is being checked
446     * @return array filtered list of files
447     */
448    static function filterDocumentFiles($user, $files) { /* {{{ */
449        $tmp = array();
450        if($files) {
451            foreach ($files as $file)
452                if ($file->isPublic() || ($file->getUser()->getID() == $user->getID()) || $user->isAdmin() || ($file->getDocument()->getOwner()->getID() == $user->getID()))
453                    array_push($tmp, $file);
454        }
455        return $tmp;
456    } /* }}} */
457
458    /** @noinspection PhpUndefinedClassInspection */
459    /**
460     * Create a new instance of the dms
461     *
462     * @param SeedDMS_Core_DatabaseAccess $db object of class {@see SeedDMS_Core_DatabaseAccess}
463     *        to access the underlying database
464     * @param string $contentDir path in filesystem containing the data store
465     *        all document contents is stored
466     */
467    function __construct($db, $contentDir) { /* {{{ */
468        $this->db = $db;
469        if(is_object($contentDir)) {
470            $this->storage = $contentDir;
471        } else {
472            $this->storage = null;
473            if(substr($contentDir, -1) == DIRECTORY_SEPARATOR)
474                $this->contentDir = $contentDir;
475            else
476                $this->contentDir = $contentDir.DIRECTORY_SEPARATOR;
477        }
478        $this->rootFolderID = 1;
479        $this->user = null;
480        $this->maxDirID = 0; //31998;
481        $this->forceRename = false;
482        $this->forceLink = false;
483        $this->checkWithinRootDir = false;
484        $this->noReadForStatus = array();
485        $this->user = null;
486        $this->classnames = array();
487        $this->classnames['folder'] = 'SeedDMS_Core_Folder';
488        $this->classnames['document'] = 'SeedDMS_Core_Document';
489        $this->classnames['documentcontent'] = 'SeedDMS_Core_DocumentContent';
490        $this->classnames['documentfile'] = 'SeedDMS_Core_DocumentFile';
491        $this->classnames['user'] = 'SeedDMS_Core_User';
492        $this->classnames['role'] = 'SeedDMS_Core_Role';
493        $this->classnames['group'] = 'SeedDMS_Core_Group';
494        $this->classnames['transmittal'] = 'SeedDMS_Core_Transmittal';
495        $this->classnames['transmittalitem'] = 'SeedDMS_Core_TransmittalItem';
496        $this->usecache = false;
497        $this->cache['users'] = [];
498        $this->callbacks = array();
499        $this->lasterror = '';
500        $this->version = '@package_version@';
501        if($this->version[0] == '@')
502            $this->version = '6.0.29';
503    } /* }}} */
504
505    /**
506     * Return class name of classes instanciated by SeedDMS_Core
507     *
508     * This method returns the class name of those objects being instantiated
509     * by the dms. Each class has an internal place holder, which must be
510     * passed to function.
511     *
512     * @param string $objectname placeholder (can be one of 'folder', 'document',
513     * 'documentcontent', 'user', 'group')
514     *
515     * @return string/boolean name of class or false if object name is invalid
516     */
517    function getClassname($objectname) { /* {{{ */
518        if(isset($this->classnames[$objectname]))
519            return $this->classnames[$objectname];
520        else
521            return false;
522    } /* }}} */
523
524    /**
525     * Set class name of instantiated objects
526     *
527     * This method sets the class name of those objects being instatiated
528     * by the dms. It is mainly used to create a new class (possible
529     * inherited from one of the available classes) implementing new
530     * features. The method should be called in the postInitDMS hook.
531     *
532     * @param string $objectname placeholder (can be one of 'folder', 'document',
533     * 'documentcontent', 'user', 'group'
534     * @param string $classname name of class
535     *
536     * @return string/boolean name of old class or false if not set
537     */
538    function setClassname($objectname, $classname) { /* {{{ */
539        if(isset($this->classnames[$objectname]))
540            $oldclass =  $this->classnames[$objectname];
541        else
542            $oldclass = false;
543        $this->classnames[$objectname] = $classname;
544        return $oldclass;
545    } /* }}} */
546
547    /**
548     * Return list of decorators
549     *
550     * This method returns the list of decorator class names of those objects
551     * being instantiated
552     * by the dms. Each class has an internal place holder, which must be
553     * passed to function.
554     *
555     * @param string $objectname placeholder (can be one of 'folder', 'document',
556     * 'documentcontent', 'user', 'group')
557     *
558     * @return array/boolean list of class names or false if object name is invalid
559     */
560    function getDecorators($objectname) { /* {{{ */
561        if(isset($this->decorators[$objectname]))
562            return $this->decorators[$objectname];
563        else
564            return false;
565    } /* }}} */
566
567    /**
568     * Add a decorator
569     *
570     * This method adds a single decorator class name to the list of decorators
571     * of those objects being instantiated
572     * by the dms. Each class has an internal place holder, which must be
573     * passed to function.
574     *
575     * @param string $objectname placeholder (can be one of 'folder', 'document',
576     * 'documentcontent', 'user', 'group')
577     *
578     * @return boolean true if decorator could be added, otherwise false
579     */
580    function addDecorator($objectname, $decorator) { /* {{{ */
581        $this->decorators[$objectname][] = $decorator;
582        return true;
583    } /* }}} */
584
585    /**
586     * Return database where meta data is stored
587     *
588     * This method returns the database object as it was set by the first
589     * parameter of the constructor.
590     *
591     * @return SeedDMS_Core_DatabaseAccess database
592     */
593    function getDB() { /* {{{ */
594        return $this->db;
595    } /* }}} */
596
597    /**
598     * Return storage where files are stored
599     *
600     * This method returns the storage object as it was set by the second
601     * parameter of the constructor.
602     *
603     * @return SeedDMS_Core_Storage
604     */
605    function getStorage() { /* {{{ */
606        return $this->storage;
607    } /* }}} */
608
609    /**
610     * Return the database version
611     *
612     * @return array|bool
613     */
614    function getDBVersion() { /* {{{ */
615        $tbllist = $this->db->TableList();
616        $tbllist = explode(',',strtolower(join(',',$tbllist)));
617        if(!in_array('tblversion', $tbllist))
618            return false;
619        $queryStr = "SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1";
620        $resArr = $this->db->getResultArray($queryStr);
621        if (is_bool($resArr) && $resArr == false)
622            return false;
623        if (count($resArr) != 1)
624            return false;
625        $resArr = $resArr[0];
626        return $resArr;
627    } /* }}} */
628
629    /**
630     * Check if the version in the database is the same as of this package
631     * Only the major and minor version number will be checked.
632     *
633     * @return boolean returns false if versions do not match, but returns
634     *         true if version matches or table tblVersion does not exists.
635     */
636    function checkVersion() { /* {{{ */
637        $tbllist = $this->db->TableList();
638        $tbllist = explode(',',strtolower(join(',',$tbllist)));
639        if(!in_array('tblversion', $tbllist))
640            return true;
641        $queryStr = "SELECT * FROM `tblVersion` ORDER BY `major`,`minor`,`subminor` LIMIT 1";
642        $resArr = $this->db->getResultArray($queryStr);
643        if (is_bool($resArr) && $resArr == false)
644            return false;
645        if (count($resArr) != 1)
646            return false;
647        $resArr = $resArr[0];
648        $ver = explode('.', $this->version);
649        if(($resArr['major'] != $ver[0]) || ($resArr['minor'] != $ver[1]))
650            return false;
651        return true;
652    } /* }}} */
653
654    /**
655     * Set id of root folder
656     *
657     * This method must be called right after creating an instance of
658     * {@see SeedDMS_Core_DMS}
659     *
660     * The new root folder id will only be set if the folder actually
661     * exists. In that case the old root folder id will be returned.
662     * If it does not exists, the method will return false;
663     * @param integer $id id of root folder
664     * @return boolean/int old root folder id if new root folder exists, otherwise false
665     */
666    function setRootFolderID($id) { /* {{{ */
667        if($this->getFolder($id)) {
668            $oldid = $this->rootFolderID;
669            $this->rootFolderID = $id;
670            return $oldid;
671        }
672        return false;
673    } /* }}} */
674
675    /**
676     * Set maximum number of subdirectories per directory
677     *
678     * The value of maxDirID is quite crucial, because each document is
679     * stored within a directory in the filesystem. Consequently, there can be
680     * a maximum number of documents, because depending on the file system
681     * the maximum number of subdirectories is limited. Since version 3.3.0 of
682     * SeedDMS an additional directory level has been introduced, which
683     * will be created when maxDirID is not 0. All documents
684     * from 1 to maxDirID-1 will be saved in 1/<docid>, documents from maxDirID
685     * to 2*maxDirID-1 are stored in 2/<docid> and so on.
686     *
687     * Modern file systems like ext4 do not have any restrictions on the number
688     * of subdirectories anymore. Therefore it is best if this parameter is
689     * set to 0. Never change this parameter if documents has already been
690     * created.
691     *
692     * This method must be called right after creating an instance of
693     * {@see SeedDMS_Core_DMS}
694     *
695     * @param integer $id id of root folder
696     */
697    function setMaxDirID($id) { /* {{{ */
698        $this->maxDirID = $id;
699    } /* }}} */
700
701    /**
702     * Get root folder
703     *
704     * @return SeedDMS_Core_Folder|boolean return the object of the root folder or false if
705     *        the root folder id was not set before with {@see SeedDMS_Core_DMS::setRootFolderID()}.
706     */
707    function getRootFolder() { /* {{{ */
708        if(!$this->rootFolderID) return false;
709        return $this->getFolder($this->rootFolderID);
710    } /* }}} */
711
712    function setForceRename($enable) { /* {{{ */
713        $this->forceRename = $enable;
714    } /* }}} */
715
716    function setForceLink($enable) { /* {{{ */
717        $this->forceLink = $enable;
718    } /* }}} */
719
720    /**
721     * Set the logged in user
722     *
723     * This method tells SeeDMS_Core_DMS the currently logged in user. It must be
724     * called right after instanciating the class, because some methods in
725     * SeedDMS_Core_Document() require the currently logged in user.
726     *
727     * @param object $user this muss not be empty and an instance of SeedDMS_Core_User
728     * @return bool|object returns the old user object or null on success, otherwise false
729     *
730     */
731    function setUser($user) { /* {{{ */
732        if(!$user) {
733            $olduser = $this->user;
734            $this->user = null;
735            return $olduser;
736        }
737        if(is_object($user) && (get_class($user) == $this->getClassname('user'))) {
738            $olduser = $this->user;
739            $this->user = $user;
740            return $olduser;
741        }
742        return false;
743    } /* }}} */
744
745    /**
746     * Get the logged in user
747     *
748     * Returns the currently logged in user, previously set by {@see SeedDMS_Core_DMS::setUser()}
749     *
750     * @return SeedDMS_Core_User $user
751     *
752     */
753    function getLoggedInUser() { /* {{{ */
754        return $this->user;
755    } /* }}} */
756
757    /**
758     * Return a document by its id
759     *
760     * This method retrieves a document from the database by its id.
761     *
762     * @param integer $id internal id of document
763     * @return SeedDMS_Core_Document instance of {@see SeedDMS_Core_Document}, null or false
764     */
765    function getDocument($id) { /* {{{ */
766        $classname = $this->classnames['document'];
767        return $classname::getInstance($id, $this);
768    } /* }}} */
769
770    /**
771     * Returns all documents of a given user
772     *
773     * @param object $user
774     * @return array list of documents
775     */
776    function getDocumentsByUser($user) { /* {{{ */
777        return $user->getDocuments();
778    } /* }}} */
779
780    /**
781     * Returns all documents locked by a given user
782     *
783     * @param object $user
784     * @return array list of documents
785     */
786    function getDocumentsLockedByUser($user) { /* {{{ */
787        return $user->getDocumentsLocked();
788    } /* }}} */
789
790    /**
791     * Returns all documents which already expired or will expire in the future
792     *
793     * The parameter $date will be relative to the start of the day. It can
794     * be either a number of days (if an integer is passed) or a date string
795     * in the format 'YYYY-MM-DD'.
796     * If the parameter $date is a negative number or a date in the past, then
797     * all documents from the start of that date till the end of the current
798     * day will be returned. If $date is a positive integer or $date is a
799     * date in the future, then all documents from the start of the current
800     * day till the end of the day of the given date will be returned.
801     * Passing 0 or the
802     * current date in $date, will return all documents expiring the current
803     * day.
804     * @param string $date date in format YYYY-MM-DD or an integer with the number
805     *   of days. A negative value will cover the days in the past.
806     * @param SeedDMS_Core_User $user limits the documents on those owned
807     *   by this user
808     * @param string $orderby n=name, e=expired
809     * @param string $orderdir d=desc or a=asc
810     * @param bool $update update status of document if set to true
811     * @return bool|SeedDMS_Core_Document[]
812     */
813    function getDocumentsExpired($date, $user=null, $orderby='e', $orderdir='desc', $update=true) { /* {{{ */
814        $db = $this->getDB();
815
816        if (!$db->createTemporaryTable("ttstatid") || !$db->createTemporaryTable("ttcontentid")) {
817            return false;
818        }
819
820        $tsnow = mktime(0, 0, 0); /* Start of today */
821        if(is_int($date) || is_string($date)) {
822            if(is_int($date)) {
823                $ts = $tsnow + $date * 86400;
824            } else {
825                $tmp = explode('-', $date, 3);
826                if(count($tmp) != 3)
827                    return false;
828                if(!self::checkDate($date, 'Y-m-d'))
829                    return false;
830                $ts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
831            }
832            if($ts < $tsnow) { /* Check for docs expired in the past */
833                $startts = $ts;
834                $endts = $tsnow+86400; /* Use end of day */
835                $updatestatus = $update;
836            } else { /* Check for docs which will expire in the future */
837                $startts = $tsnow;
838                $endts = $ts+86400; /* Use end of day */
839                $updatestatus = false;
840            }
841        }    elseif(is_array($date)) { // start and end date
842            if(!empty($date['start'])) {
843                if(is_int($date['start']))
844                    $startts = $date['start'];
845                else {
846                    $tmp = explode('-', $date['start'], 3);
847                    if(count($tmp) != 3)
848                        return false;
849                    if(!self::checkDate($date, 'Y-m-d'))
850                        return false;
851                    $startts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
852                }
853            } else {
854                $startts = time();
855            }
856            if(!empty($date['end'])) {
857                if(is_int($date['end']))
858                    $endts = $date['end'];
859                else {
860                    $tmp = explode('-', $date['end'], 3);
861                    if(count($tmp) != 3)
862                        return false;
863                    if(!self::checkDate($date, 'Y-m-d'))
864                        return false;
865                    $endts = mktime(24, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
866                }
867            } else {
868                $endts = time() + 365*86400;
869            }
870            if(($startts < $tsnow) && ($endts < $tsnow))
871                $updatestatus = $update;
872            else
873                $updatestatus = false;
874        } else
875            return false;
876
877        /* Get all documents which have an expiration date. It doesn't check for
878         * the latest status which should be S_EXPIRED, but doesn't have to, because
879         * status may have not been updated after the expiration date has been reached.
880         **/
881        $queryStr = "SELECT `tblDocuments`.`id`, `tblDocumentStatusLog`.`status`  FROM `tblDocuments` ".
882            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`document` = `tblDocuments`.`id` ".
883            "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
884            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` AND `tblDocumentContent`.`version` = `tblDocumentStatus`.`version` ".
885            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
886            "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusLogID` = `ttstatid`.`maxLogID`";
887        $queryStr .= 
888            " WHERE `tblDocuments`.`expires` >= ".$startts." AND `tblDocuments`.`expires` < ".$endts;
889        if($user)
890            $queryStr .=
891                " AND `tblDocuments`.`owner` = '".$user->getID()."' ";
892        $queryStr .= 
893            " ORDER BY ".($orderby == 'e' ? "`expires`" : "`name`")." ".($orderdir == 'd' ? "DESC" : "ASC");
894
895        $resArr = $db->getResultArray($queryStr);
896        if (is_bool($resArr) && !$resArr)
897            return false;
898
899        /** @var SeedDMS_Core_Document[] $documents */
900        $documents = array();
901        foreach ($resArr as $row) {
902            $document = $this->getDocument($row["id"]);
903            if($updatestatus) {
904                $document->verifyLastestContentExpriry();
905            }
906            $documents[] = $document;
907        }
908        return $documents;
909    } /* }}} */
910
911    /**
912     * Returns a document by its name
913     *
914     * This method searches a document by its name and restricts the search
915     * to the given folder if passed as the second parameter.
916     * If there are more than one document with that name, then only the
917     * one with the highest id will be returned. 
918     *
919     * @param string $name Name of the document
920     * @param object $folder parent folder of document
921     * @return SeedDMS_Core_Document|null|boolean found document or null if not document was found or false in case of an error
922     */
923    function getDocumentByName($name, $folder=null) { /* {{{ */
924        $name = trim($name);
925        if (!$name) return false;
926
927        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser` ".
928            "FROM `tblDocuments` ".
929            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
930            "WHERE `tblDocuments`.`name` = " . $this->db->qstr($name);
931        if($folder)
932            $queryStr .= " AND `tblDocuments`.`folder` = ". $folder->getID();
933        if($this->checkWithinRootDir)
934            $queryStr .= " AND `tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
935        $queryStr .= " ORDER BY `tblDocuments`.`id` DESC LIMIT 1";
936
937        $resArr = $this->db->getResultArray($queryStr);
938        if (is_bool($resArr) && !$resArr)
939            return false;
940
941        if(!$resArr)
942            return null;
943
944        $row = $resArr[0];
945        /** @var SeedDMS_Core_Document $document */
946        $document = new $this->classnames['document']($row["id"], $row["name"], $row["comment"], $row["date"], $row["expires"], $row["owner"], $row["folder"], $row["inheritAccess"], $row["defaultAccess"], $row["lockUser"], $row["keywords"], $row["sequence"]);
947        $document->setDMS($this);
948        return $document;
949    } /* }}} */
950
951    /**
952     * Returns a document by the original file name of the last version
953     *
954     * This method searches a document by the name of the last document
955     * version and restricts the search
956     * to given folder if passed as the second parameter.
957     * If there are more than one document with that name, then only the
958     * one with the highest id will be returned. 
959     *
960     * @param string $name Name of the original file
961     * @param object $folder parent folder of document
962     * @return SeedDMS_Core_Document|null|boolean found document or null if not document was found or false in case of an error
963     */
964    function getDocumentByOriginalFilename($name, $folder=null) { /* {{{ */
965        $name = trim($name);
966        if (!$name) return false;
967
968        if (!$this->db->createTemporaryTable("ttcontentid")) {
969            return false;
970        }
971        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser` ".
972            "FROM `tblDocuments` ".
973            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`document` = `tblDocuments`.`id` ".
974            "LEFT JOIN `tblDocumentContent` ON `tblDocumentContent`.`document` = `tblDocuments`.`id` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
975            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
976            "WHERE `tblDocumentContent`.`orgFileName` = " . $this->db->qstr($name);
977        if($folder)
978            $queryStr .= " AND `tblDocuments`.`folder` = ". $folder->getID();
979        $queryStr .= " ORDER BY `tblDocuments`.`id` DESC LIMIT 1";
980
981        $resArr = $this->db->getResultArray($queryStr);
982        if (is_bool($resArr) && !$resArr)
983            return false;
984
985        if(!$resArr)
986            return null;
987
988        $row = $resArr[0];
989        /** @var SeedDMS_Core_Document $document */
990        $document = new $this->classnames['document']($row["id"], $row["name"], $row["comment"], $row["date"], $row["expires"], $row["owner"], $row["folder"], $row["inheritAccess"], $row["defaultAccess"], $row["lockUser"], $row["keywords"], $row["sequence"]);
991        $document->setDMS($this);
992        return $document;
993    } /* }}} */
994
995    /**
996     * Return a document content by its id
997     *
998     * This method retrieves a document content from the database by its id.
999     *
1000     * @param integer $id internal id of document content
1001     * @return bool|null|SeedDMS_Core_DocumentContent found document content or null if not document content was found or false in case of an error
1002
1003     */
1004    function getDocumentContent($id) { /* {{{ */
1005        $classname = $this->classnames['documentcontent'];
1006        return $classname::getInstance($id, $this);
1007    } /* }}} */
1008
1009    /**
1010     * Returns all documents with a predefined search criteria
1011     *
1012     * @param string $listtype type of document list, can be 'AppRevByMe',
1013     * 'AppRevOwner', 'ReceiptByMe', 'ReviseByMe', 'LockedByMe', 'MyDocs'
1014     * @param object $user user
1015     * @return array list of documents records
1016     */
1017    function countTasks($listtype, $user=null, $param5=true) { /* {{{ */
1018        if (!$this->db->createTemporaryTable("ttstatid") || !$this->db->createTemporaryTable("ttcontentid")) {
1019            return false;
1020        }
1021        $groups = array();
1022        if($user) {
1023            $tmp = $user->getGroups();
1024            foreach($tmp as $group)
1025                $groups[] = $group->getID();
1026        }
1027        $selectStr = "count(distinct ttcontentid.document) c ";
1028        $queryStr = 
1029            "FROM `ttcontentid` ".
1030            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID`=`ttcontentid`.`document` AND `tblDocumentStatus`.`version`=`ttcontentid`.`maxVersion` ".
1031            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
1032            "LEFT JOIN `tblDocumentStatusLog` ON `ttstatid`.`statusID` = `tblDocumentStatusLog`.`statusID` AND `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ";
1033        switch($listtype) {
1034        case 'ReviewByMe': // Documents I have to review {{{
1035            if (!$this->db->createTemporaryTable("ttreviewid")) {
1036                return false;
1037            }
1038            $queryStr .=
1039                "LEFT JOIN `tblDocumentReviewers` on `ttcontentid`.`document`=`tblDocumentReviewers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentReviewers`.`version` ".
1040                "LEFT JOIN `ttreviewid` ON `ttreviewid`.`reviewID` = `tblDocumentReviewers`.`reviewID` ".
1041                "LEFT JOIN `tblDocumentReviewLog` ON `tblDocumentReviewLog`.`reviewLogID`=`ttreviewid`.`maxLogID` ";
1042
1043            $queryStr .= "WHERE (`tblDocumentReviewers`.`type` = 0 AND `tblDocumentReviewers`.`required` = ".$user->getID()." ";
1044            if($groups)
1045                $queryStr .= "OR `tblDocumentReviewers`.`type` = 1 AND `tblDocumentReviewers`.`required` IN (".implode(',', $groups).") ";
1046            $queryStr .= ") ";
1047            $queryStr .= "AND `tblDocumentReviewLog`.`status` = 0 ";
1048            $docstatarr = array(S_DRAFT_REV);
1049            if($param5)
1050                $docstatarr[] = S_EXPIRED;
1051            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1052            break; /* }}} */
1053        case 'ApproveByMe': // Documents I have to approve {{{
1054            if (!$this->db->createTemporaryTable("ttapproveid")) {
1055                return false;
1056            }
1057            $queryStr .=
1058                "LEFT JOIN `tblDocumentApprovers` on `ttcontentid`.`document`=`tblDocumentApprovers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentApprovers`.`version` ".
1059                "LEFT JOIN `ttapproveid` ON `ttapproveid`.`approveID` = `tblDocumentApprovers`.`approveID` ".
1060                "LEFT JOIN `tblDocumentApproveLog` ON `tblDocumentApproveLog`.`approveLogID`=`ttapproveid`.`maxLogID` ";
1061
1062            if($user) {
1063                $queryStr .= "WHERE (`tblDocumentApprovers`.`type` = 0 AND `tblDocumentApprovers`.`required` = ".$user->getID()." ";
1064                if($groups)
1065                    $queryStr .= "OR `tblDocumentApprovers`.`type` = 1 AND `tblDocumentApprovers`.`required` IN (".implode(',', $groups).") ";
1066                $queryStr .= ") ";
1067            }
1068            $queryStr .= "AND `tblDocumentApproveLog`.`status` = 0 ";
1069            $docstatarr = array(S_DRAFT_APP);
1070            if($param5)
1071                $docstatarr[] = S_EXPIRED;
1072            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1073            break; /* }}} */
1074        case 'ReceiptByMe': // Documents I have to receipt {{{
1075            if (!$this->db->createTemporaryTable("ttreceiptid")) {
1076                return false;
1077            }
1078            $queryStr .=
1079                "LEFT JOIN `tblDocumentRecipients` on `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` ".
1080                "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`receiptID` = `tblDocumentRecipients`.`receiptID` ".
1081                "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentReceiptLog`.`receiptLogID`=`ttreceiptid`.`maxLogID` ";
1082
1083            if($user) {
1084                $queryStr .= "WHERE (`tblDocumentRecipients`.`type` = 0 AND `tblDocumentRecipients`.`required` = ".$user->getID()." ";
1085                if($groups)
1086                    $queryStr .= "OR `tblDocumentRecipients`.`type` = 1 AND `tblDocumentRecipients`.`required` IN (".implode(',', $groups).") ";
1087                $queryStr .= ") ";
1088            }
1089            $queryStr .= "AND `tblDocumentReceiptLog`.`status` = 0 ";
1090            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") ";
1091            break; /* }}} */
1092        case 'ReviseByMe': // Documents I have to receipt {{{
1093            if (!$this->db->createTemporaryTable("ttrevisionid")) {
1094                return false;
1095            }
1096            $queryStr .=
1097                "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ".
1098                "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ".
1099                "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` ";
1100
1101            if($user) {
1102                $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." ";
1103                if($groups)
1104                    $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).") ";
1105                $queryStr .= ") ";
1106            }
1107            $queryStr .= "AND `tblDocumentRevisionLog`.`status` = 0 ";
1108            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_IN_REVISION.") ";
1109            break; /* }}} */
1110        case 'SleepingReviseByMe': // Documents I have to receipt {{{
1111            if (!$this->db->createTemporaryTable("ttrevisionid")) {
1112                return false;
1113            }
1114            $queryStr .=
1115                "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ".
1116                "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ".
1117                "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` ";
1118
1119            if($user) {
1120                $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." ";
1121                if($groups)
1122                    $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).") ";
1123                $queryStr .= ") ";
1124            }
1125            $queryStr .= "AND `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime(14)." ";
1126            $queryStr .= "AND `tblDocumentRevisionLog`.`status` = -3 ";
1127            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") ";
1128            break; /* }}} */
1129        case 'NeedsCorrectionOwner': // Documents that need to be corrected {{{
1130            $queryStr .=
1131                "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `ttcontentid`.`document` ";
1132            $queryStr .= "WHERE `tblDocuments`.`owner` = '".$user->getID()."' ".
1133                "AND `tblDocumentStatusLog`.`status` IN (".S_NEEDS_CORRECTION.") ";
1134            break; /* }}} */
1135        case 'WorkflowByMe': // Documents which need my workflow action {{{
1136
1137            $queryStr .=
1138                "LEFT JOIN `tblWorkflowDocumentContent` on `ttcontentid`.`document`=`tblWorkflowDocumentContent`.`document` AND `ttcontentid`.`maxVersion`=`tblWorkflowDocumentContent`.`version` ".
1139                "LEFT JOIN `tblWorkflowTransitions` on `tblWorkflowDocumentContent`.`workflow`=`tblWorkflowTransitions`.`workflow` AND `tblWorkflowDocumentContent`.`state`=`tblWorkflowTransitions`.`state` ".
1140                "LEFT JOIN `tblWorkflowTransitionUsers` on `tblWorkflowTransitionUsers`.`transition` = `tblWorkflowTransitions`.`id` ".
1141                "LEFT JOIN `tblWorkflowTransitionGroups` on `tblWorkflowTransitionGroups`.`transition` = `tblWorkflowTransitions`.`id` ";
1142
1143            if($user) {
1144                $queryStr .= "WHERE (`tblWorkflowTransitionUsers`.`userid` = ".$user->getID()." ";
1145                if($groups)
1146                    $queryStr .= "OR `tblWorkflowTransitionGroups`.`groupid` IN (".implode(',', $groups).")";
1147                $queryStr .= ") ";
1148            }
1149            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_WORKFLOW." ";
1150            break; // }}}
1151        }
1152        if($queryStr) {
1153            $resArr = $this->db->getResultArray('SELECT '.$selectStr.$queryStr);
1154            if (is_bool($resArr) && !$resArr) {
1155                return false;
1156            }
1157        } else {
1158            return false;
1159        }
1160        return $resArr[0]['c'];
1161    } /* }}} */
1162
1163    /**
1164     * Returns all documents with a predefined search criteria
1165     *
1166     * The records return have the following elements
1167     *
1168     * From Table tblDocuments
1169     * [id] => id of document
1170     * [name] => name of document
1171     * [comment] => comment of document
1172     * [date] => timestamp of creation date of document
1173     * [expires] => timestamp of expiration date of document
1174     * [owner] => user id of owner
1175     * [folder] => id of parent folder
1176     * [folderList] => column separated list of folder ids, e.g. :1:41:
1177     * [inheritAccess] => 1 if access is inherited
1178     * [defaultAccess] => default access mode
1179     * [locked] => always -1 (TODO: is this field still used?)
1180     * [keywords] => keywords of document
1181     * [sequence] => sequence of document
1182     *
1183     * From Table tblDocumentLocks
1184     * [lockUser] => id of user locking the document
1185     *
1186     * From Table tblDocumentStatusLog
1187     * [version] => latest version of document
1188     * [statusID] => id of latest status log
1189     * [documentID] => id of document
1190     * [status] => current status of document
1191     * [statusComment] => comment of current status
1192     * [statusDate] => datetime when the status was entered, e.g. 2014-04-17 21:35:51
1193     * [userID] => id of user who has initiated the status change
1194     *
1195     * From Table tblUsers
1196     * [ownerName] => name of owner of document
1197     * [statusName] => name of user who has initiated the status change
1198     *
1199     * @param string $listtype type of document list, can be 'AppRevByMe',
1200     * 'AppRevOwner', 'ReceiptByMe', 'ReviseByMe', 'LockedByMe', 'MyDocs'
1201     * @param SeedDMS_Core_User $param1 user
1202     * @param bool|integer|string $param2 if set to true
1203     * 'ReviewByMe', 'ApproveByMe', 'AppRevByMe', 'ReviseByMe', 'ReceiptByMe'
1204     * will also return documents which the reviewer, approver, etc.
1205     * has already taken care of. If set to false only
1206     * untouched documents will be returned. In case of 'ExpiredOwner',
1207     * 'SleepingReviseByMe' this
1208     * parameter contains the number of days (a negative number is allowed)
1209     * relativ to the current date or a date in format 'yyyy-mm-dd'
1210     * (even in the past).
1211     * @param string $param3 sort list by this field
1212     * @param string $param4 order direction
1213     * @param bool $param5 set to false if expired documents shall not be considered
1214     * @return array|bool
1215     */
1216    function getDocumentList($listtype, $param1=null, $param2=false, $param3='', $param4='', $param5=true) { /* {{{ */
1217        /* The following query will get all documents and lots of additional
1218         * information. It requires the two temporary tables ttcontentid and
1219         * ttstatid.
1220         */
1221        if (!$this->db->createTemporaryTable("ttstatid") || !$this->db->createTemporaryTable("ttcontentid")) {
1222            return false;
1223        }
1224        /* The following statement retrieves the status of the last version of all
1225         * documents. It must be restricted by further where clauses.
1226         */
1227/*
1228        $queryStr = "SELECT `tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser`, ".
1229            "`tblDocumentContent`.`version`, `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
1230            "`tblDocumentStatusLog`.`comment` AS `statusComment`, `tblDocumentStatusLog`.`date` as `statusDate`, ".
1231            "`tblDocumentStatusLog`.`userID`, `oTbl`.`fullName` AS `ownerName`, `sTbl`.`fullName` AS `statusName` ".
1232            "FROM `tblDocumentContent` ".
1233            "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ".
1234            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` ".
1235            "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusID` = `tblDocumentStatus`.`statusID` ".
1236            "LEFT JOIN `ttstatid` ON `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ".
1237            "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ".
1238            "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
1239            "LEFT JOIN `tblUsers` AS `oTbl` on `oTbl`.`id` = `tblDocuments`.`owner` ".
1240            "LEFT JOIN `tblUsers` AS `sTbl` on `sTbl`.`id` = `tblDocumentStatusLog`.`userID` ".
1241            "WHERE `ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` ".
1242            "AND `ttcontentid`.`maxVersion` = `tblDocumentContent`.`version` ";
1243 */
1244        /* New sql statement which retrieves all documents, its latest version and
1245         * status, the owner and user initiating the latest status.
1246         * It doesn't need the where clause anymore. Hence the statement could be
1247         * extended with further left joins.
1248         */
1249        $selectStr = "`tblDocuments`.*, `tblDocumentLocks`.`userID` as `lockUser`, ".
1250            "`tblDocumentContent`.`version`, `tblDocumentStatus`.*, `tblDocumentStatusLog`.`status`, ".
1251            "`tblDocumentStatusLog`.`comment` AS `statusComment`, `tblDocumentStatusLog`.`date` as `statusDate`, ".
1252            "`tblDocumentStatusLog`.`userID`, `oTbl`.`fullName` AS `ownerName`, `sTbl`.`fullName` AS `statusName` ";
1253        $queryStr =
1254            "FROM `ttcontentid` ".
1255            "LEFT JOIN `tblDocuments` ON `tblDocuments`.`id` = `ttcontentid`.`document` ".
1256            "LEFT JOIN `tblDocumentContent` ON `tblDocumentContent`.`document` = `ttcontentid`.`document` AND `tblDocumentContent`.`version` = `ttcontentid`.`maxVersion` ".
1257            "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID`=`ttcontentid`.`document` AND `tblDocumentStatus`.`version`=`ttcontentid`.`maxVersion` ".
1258            "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
1259            "LEFT JOIN `tblDocumentStatusLog` ON `ttstatid`.`statusID` = `tblDocumentStatusLog`.`statusID` AND `ttstatid`.`maxLogID` = `tblDocumentStatusLog`.`statusLogID` ".
1260            "LEFT JOIN `tblDocumentLocks` ON `ttcontentid`.`document`=`tblDocumentLocks`.`document` ".
1261            "LEFT JOIN `tblUsers` `oTbl` ON `oTbl`.`id` = `tblDocuments`.`owner` ".
1262            "LEFT JOIN `tblUsers` `sTbl` ON `sTbl`.`id` = `tblDocumentStatusLog`.`userID` ";
1263
1264//        echo $queryStr;
1265
1266        switch($listtype) {
1267        case 'AppRevByMe': // Documents I have to review/approve {{{
1268            $queryStr .= "WHERE 1=1 ";
1269
1270            $user = $param1;
1271            // Get document list for the current user.
1272            $reviewStatus = $user->getReviewStatus();
1273            $approvalStatus = $user->getApprovalStatus();
1274
1275            // Create a comma separated list of all the documentIDs whose information is
1276            // required.
1277            // Take only those documents into account which hasn't be touched by the user
1278            $dList = array();
1279            foreach ($reviewStatus["indstatus"] as $st) {
1280                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1281                    $dList[] = $st["documentID"];
1282                }
1283            }
1284            foreach ($reviewStatus["grpstatus"] as $st) {
1285                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1286                    $dList[] = $st["documentID"];
1287                }
1288            }
1289            foreach ($approvalStatus["indstatus"] as $st) {
1290                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1291                    $dList[] = $st["documentID"];
1292                }
1293            }
1294            foreach ($approvalStatus["grpstatus"] as $st) {
1295                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1296                    $dList[] = $st["documentID"];
1297                }
1298            }
1299            $docCSV = "";
1300            foreach ($dList as $d) {
1301                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1302            }
1303
1304            if (strlen($docCSV)>0) {
1305                $docstatarr = array(S_DRAFT_REV, S_DRAFT_APP);
1306                if($param5)
1307                    $docstatarr[] = S_EXPIRED;
1308                $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ".
1309                            "AND `tblDocuments`.`id` IN (" . $docCSV . ") ".
1310                            "ORDER BY `statusDate` DESC";
1311            } else {
1312                $queryStr = '';
1313            }
1314            break; // }}}
1315        case 'ReviewByMe': // Documents I have to review {{{
1316            if (!$this->db->createTemporaryTable("ttreviewid")) {
1317                return false;
1318            }
1319            $user = $param1;
1320            $orderby = $param3;
1321            if($param4 == 'desc')
1322                $orderdir = 'DESC';
1323            else
1324                $orderdir = 'ASC';
1325
1326            $groups = array();
1327            if($user) {
1328                $tmp = $user->getGroups();
1329                foreach($tmp as $group)
1330                    $groups[] = $group->getID();
1331            }
1332
1333            $selectStr .= ", `tblDocumentReviewLog`.`date` as `duedate` ";
1334            $queryStr .=
1335                "LEFT JOIN `tblDocumentReviewers` ON `ttcontentid`.`document`=`tblDocumentReviewers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentReviewers`.`version` ".
1336                "LEFT JOIN `ttreviewid` ON `ttreviewid`.`reviewID` = `tblDocumentReviewers`.`reviewID` ".
1337                "LEFT JOIN `tblDocumentReviewLog` ON `tblDocumentReviewLog`.`reviewLogID`=`ttreviewid`.`maxLogID` ";
1338
1339            if(1) {
1340            if($user) {
1341                $queryStr .= "WHERE (`tblDocumentReviewers`.`type` = 0 AND `tblDocumentReviewers`.`required` = ".$user->getID()." ";
1342                if($groups)
1343                    $queryStr .= "OR `tblDocumentReviewers`.`type` = 1 AND `tblDocumentReviewers`.`required` IN (".implode(',', $groups).") ";
1344                $queryStr .= ") ";
1345            }
1346            $docstatarr = array(S_DRAFT_REV);
1347            if($param5)
1348                $docstatarr[] = S_EXPIRED;
1349            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1350            if(!$param2)
1351                $queryStr .= " AND `tblDocumentReviewLog`.`status` = 0 ";
1352            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1353            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1354            else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1355            else $queryStr .= "ORDER BY `name`";
1356            $queryStr .= " ".$orderdir;
1357            } else {
1358            $queryStr .= "WHERE 1=1 ";
1359
1360            // Get document list for the current user.
1361            $reviewStatus = $user->getReviewStatus();
1362
1363            // Create a comma separated list of all the documentIDs whose information is
1364            // required.
1365            // Take only those documents into account which hasn't be touched by the user
1366            // ($st["status"]==0)
1367            $dList = array();
1368            foreach ($reviewStatus["indstatus"] as $st) {
1369                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1370                    $dList[] = $st["documentID"];
1371                }
1372            }
1373            foreach ($reviewStatus["grpstatus"] as $st) {
1374                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1375                    $dList[] = $st["documentID"];
1376                }
1377            }
1378            $docCSV = "";
1379            foreach ($dList as $d) {
1380                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1381            }
1382
1383            if (strlen($docCSV)>0) {
1384                $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_EXPIRED.") ".
1385                            "AND `tblDocuments`.`id` IN (" . $docCSV . ") ";
1386                //$queryStr .= "ORDER BY `statusDate` DESC";
1387                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1388                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1389                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1390                else $queryStr .= "ORDER BY `name`";
1391                $queryStr .= " ".$orderdir;
1392            } else {
1393                $queryStr = '';
1394            }
1395            }
1396            break; // }}}
1397        case 'ApproveByMe': // Documents I have to approve {{{
1398            if (!$this->db->createTemporaryTable("ttapproveid")) {
1399                return false;
1400            }
1401            $user = $param1;
1402            $orderby = $param3;
1403            if($param4 == 'desc')
1404                $orderdir = 'DESC';
1405            else
1406                $orderdir = 'ASC';
1407
1408            $groups = array();
1409            if($user) {
1410                $tmp = $user->getGroups();
1411                foreach($tmp as $group)
1412                    $groups[] = $group->getID();
1413            }
1414
1415            $selectStr .= ", `tblDocumentApproveLog`.`date` as `duedate` ";
1416            $queryStr .=
1417                "LEFT JOIN `tblDocumentApprovers` ON `ttcontentid`.`document`=`tblDocumentApprovers`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentApprovers`.`version` ".
1418                "LEFT JOIN `ttapproveid` ON `ttapproveid`.`approveID` = `tblDocumentApprovers`.`approveID` ".
1419                "LEFT JOIN `tblDocumentApproveLog` ON `tblDocumentApproveLog`.`approveLogID`=`ttapproveid`.`maxLogID` ";
1420
1421            if(1) {
1422            if($user) {
1423            $queryStr .= "WHERE (`tblDocumentApprovers`.`type` = 0 AND `tblDocumentApprovers`.`required` = ".$user->getID()." ";
1424            if($groups)
1425                $queryStr .= "OR `tblDocumentApprovers`.`type` = 1 AND `tblDocumentApprovers`.`required` IN (".implode(',', $groups).")";
1426            $queryStr .= ") ";
1427            }
1428            $docstatarr = array(S_DRAFT_APP);
1429            if($param5)
1430                $docstatarr[] = S_EXPIRED;
1431            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ";
1432            if(!$param2)
1433                $queryStr .= " AND `tblDocumentApproveLog`.`status` = 0 ";
1434            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1435            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1436            else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1437            else $queryStr .= "ORDER BY `name`";
1438            $queryStr .= " ".$orderdir;
1439            } else {
1440            $queryStr .= "WHERE 1=1 ";
1441
1442            // Get document list for the current user.
1443            $approvalStatus = $user->getApprovalStatus();
1444
1445            // Create a comma separated list of all the documentIDs whose information is
1446            // required.
1447            // Take only those documents into account which hasn't be touched by the user
1448            // ($st["status"]==0)
1449            $dList = array();
1450            foreach ($approvalStatus["indstatus"] as $st) {
1451                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1452                    $dList[] = $st["documentID"];
1453                }
1454            }
1455            foreach ($approvalStatus["grpstatus"] as $st) {
1456                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1457                    $dList[] = $st["documentID"];
1458                }
1459            }
1460            $docCSV = "";
1461            foreach ($dList as $d) {
1462                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1463            }
1464
1465            if (strlen($docCSV)>0) {
1466                $docstatarr = array(S_DRAFT_APP);
1467                if($param5)
1468                    $docstatarr[] = S_EXPIRED;
1469                $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".implode(',', $docstatarr).") ".
1470                            "AND `tblDocuments`.`id` IN (" . $docCSV . ") ";
1471                //$queryStr .= "ORDER BY `statusDate` DESC";
1472                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1473                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1474                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1475                else $queryStr .= "ORDER BY `name`";
1476                $queryStr .= " ".$orderdir;
1477            } else {
1478                $queryStr = '';
1479            }
1480            }
1481            break; // }}}
1482        case 'ReceiptByMe': // Documents I have to receipt {{{
1483            if (!$this->db->createTemporaryTable("ttreceiptid")) {
1484                return false;
1485            }
1486            $user = $param1;
1487            $orderby = $param3;
1488            if($param4 == 'desc')
1489                $orderdir = 'DESC';
1490            else
1491                $orderdir = 'ASC';
1492
1493            $groups = array();
1494            $tmp = $user->getGroups();
1495            foreach($tmp as $group)
1496                $groups[] = $group->getID();
1497
1498            $selectStr .= ", `tblDocumentReceiptLog`.`date` as `duedate` ";
1499            $queryStr .=
1500                "LEFT JOIN `tblDocumentRecipients` on `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` ".
1501                "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`receiptID` = `tblDocumentRecipients`.`receiptID` ".
1502                "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentReceiptLog`.`receiptLogID`=`ttreceiptid`.`maxLogID` ";
1503
1504            if(1) {
1505            $queryStr .= "WHERE (`tblDocumentRecipients`.`type` = 0 AND `tblDocumentRecipients`.`required` = ".$user->getID()." ";
1506            /* Checking for groups slows down the statement extremly on sqlite */
1507            if($groups)
1508                $queryStr .= "OR `tblDocumentRecipients`.`type` = 1 AND `tblDocumentRecipients`.`required` IN (".implode(',', $groups).")";
1509            $queryStr .= ") ";
1510            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." ";
1511            if(!$param2)
1512                $queryStr .= " AND `tblDocumentReceiptLog`.`status` = 0 ";
1513            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1514            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1515            else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1516            else $queryStr .= "ORDER BY `name`";
1517            $queryStr .= " ".$orderdir;
1518            } else {
1519            $queryStr .= "WHERE 1=1 ";
1520
1521            // Get document list for the current user.
1522            $receiptStatus = $user->getReceiptStatus();
1523
1524            // Create a comma separated list of all the documentIDs whose information is
1525            // required.
1526            // Take only those documents into account which hasn't be touched by the user
1527            // ($st["status"]==0)
1528            $dList = array();
1529            foreach ($receiptStatus["indstatus"] as $st) {
1530                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1531                    $dList[] = $st["documentID"];
1532                }
1533            }
1534            foreach ($receiptStatus["grpstatus"] as $st) {
1535                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1536                    $dList[] = $st["documentID"];
1537                }
1538            }
1539            $docCSV = "";
1540            foreach ($dList as $d) {
1541                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1542            }
1543
1544            if (strlen($docCSV)>0) {
1545                $queryStr .= "AND `tblDocuments`.`id` IN (" . $docCSV . ") ";
1546//                $queryStr .= "ORDER BY `statusDate` DESC";
1547                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1548                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1549                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1550                else $queryStr .= "ORDER BY `name`";
1551                $queryStr .= " ".$orderdir;
1552            } else {
1553                $queryStr = '';
1554            }
1555            }
1556            break; // }}}
1557        case 'ReviseByMe': // Documents I have to revise {{{
1558            if (!$this->db->createTemporaryTable("ttrevisionid")) {
1559                return false;
1560            }
1561            $user = $param1;
1562            $orderby = $param3;
1563            if($param4 == 'desc')
1564                $orderdir = 'DESC';
1565            else
1566                $orderdir = 'ASC';
1567
1568            $groups = array();
1569            $tmp = $user->getGroups();
1570            foreach($tmp as $group)
1571                $groups[] = $group->getID();
1572
1573            $selectStr .= ", `tblDocumentRevisionLog`.`date` as `duedate` ";
1574            $queryStr .=
1575                "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ".
1576                "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ".
1577                "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` ";
1578
1579            if(1) {
1580            $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." ";
1581            if($groups)
1582                $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).")";
1583            $queryStr .= ") ";
1584            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_REVISION." ";
1585            if(!$param2)
1586                $queryStr .= " AND `tblDocumentRevisionLog`.`status` = 0 ";
1587            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1588            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1589            else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1590            else $queryStr .= "ORDER BY `name`";
1591            $queryStr .= " ".$orderdir;
1592            } else {
1593            $queryStr .= "WHERE 1=1 ";
1594
1595            // Get document list for the current user.
1596            $revisionStatus = $user->getRevisionStatus();
1597
1598            // Create a comma separated list of all the documentIDs whose information is
1599            // required.
1600            $dList = array();
1601            foreach ($revisionStatus["indstatus"] as $st) {
1602                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1603                    $dList[] = $st["documentID"];
1604                }
1605            }
1606            foreach ($revisionStatus["grpstatus"] as $st) {
1607                if (($st["status"]==0 || $param2) && !in_array($st["documentID"], $dList)) {
1608                    $dList[] = $st["documentID"];
1609                }
1610            }
1611            $docCSV = "";
1612            foreach ($dList as $d) {
1613                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1614            }
1615
1616            if (strlen($docCSV)>0) {
1617                $queryStr .= "AND `tblDocuments`.`id` IN (" . $docCSV . ") ";
1618                //$queryStr .= "ORDER BY `statusDate` DESC";
1619                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1620                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1621                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1622                else $queryStr .= "ORDER BY `name`";
1623                $queryStr .= " ".$orderdir;
1624            } else {
1625                $queryStr = '';
1626            }
1627            }
1628            break; // }}}
1629        case 'SleepingReviseByMe': // Documents I have to revise but are still sleeping {{{
1630            if (!$this->db->createTemporaryTable("ttrevisionid")) {
1631                return false;
1632            }
1633
1634            $dayoffset = 0;
1635            if(is_int($param2)) {
1636                $dayoffset = (int) $param2;
1637            }
1638
1639            $user = $param1;
1640            $orderby = $param3;
1641            if($param4 == 'desc')
1642                $orderdir = 'DESC';
1643            else
1644                $orderdir = 'ASC';
1645
1646            $groups = array();
1647            $tmp = $user->getGroups();
1648            foreach($tmp as $group)
1649                $groups[] = $group->getID();
1650
1651            $selectStr .= ", `tblDocumentRevisionLog`.`date` as `duedate` ";
1652            $queryStr .=
1653                "LEFT JOIN `tblDocumentRevisors` on `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` AND `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` ".
1654                "LEFT JOIN `ttrevisionid` ON `ttrevisionid`.`revisionID` = `tblDocumentRevisors`.`revisionID` ".
1655                "LEFT JOIN `tblDocumentRevisionLog` ON `tblDocumentRevisionLog`.`revisionLogID`=`ttrevisionid`.`maxLogID` ";
1656
1657            $queryStr .= "WHERE (`tblDocumentRevisors`.`type` = 0 AND `tblDocumentRevisors`.`required` = ".$user->getID()." ";
1658            if($groups)
1659                $queryStr .= "OR `tblDocumentRevisors`.`type` = 1 AND `tblDocumentRevisors`.`required` IN (".implode(',', $groups).")";
1660            $queryStr .= ") ";
1661            $queryStr .= "AND `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime($dayoffset)." ";
1662            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." ";
1663            $queryStr .= " AND `tblDocumentRevisionLog`.`status` = -3 ";
1664            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1665            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1666            else if ($orderby=='s') $queryStr .= "ORDER BY `tblDocumentStatusLog`.`status`";
1667            else $queryStr .= "ORDER BY `name`";
1668            $queryStr .= " ".$orderdir;
1669            break; // }}}
1670        case 'DueRevision': // Documents with a due revision, which is not started {{{
1671            if (!$this->db->createTemporaryTable("ttrevisionid")) {
1672                return false;
1673            }
1674
1675            $dayoffset = 0;
1676            if(is_int($param2)) {
1677                $dayoffset = (int) $param2;
1678            }
1679
1680            $user = $param1;
1681            $orderby = $param3;
1682            if($param4 == 'desc')
1683                $orderdir = 'DESC';
1684            else
1685                $orderdir = 'ASC';
1686
1687            $selectStr .= ", `tblDocumentContent`.`revisiondate` ";
1688            $queryStr .= "WHERE `tblDocumentContent`.`revisiondate` IS NOT NULL AND `tblDocumentContent`.`revisiondate` <= ".$this->db->getCurrentDatetime($dayoffset)." ";
1689            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_RELEASED." ";
1690            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1691            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1692            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1693            else $queryStr .= "ORDER BY `name`";
1694            $queryStr .= " ".$orderdir;
1695            $queryStr .= ", `tblDocumentContent`.`revisiondate` ASC";
1696            break; // }}}
1697        case 'WorkflowByMe': // Documents I to trigger in Worklflow {{{
1698            $user = $param1;
1699            $orderby = $param3;
1700            if($param4 == 'desc')
1701                $orderdir = 'DESC';
1702            else
1703                $orderdir = 'ASC';
1704
1705            if(1) {
1706            $groups = array();
1707            if($user) {
1708                $tmp = $user->getGroups();
1709                foreach($tmp as $group)
1710                    $groups[] = $group->getID();
1711            }
1712            $selectStr = 'distinct '.$selectStr;
1713            $queryStr .=
1714                "LEFT JOIN `tblWorkflowDocumentContent` ON `ttcontentid`.`document`=`tblWorkflowDocumentContent`.`document` AND `ttcontentid`.`maxVersion`=`tblWorkflowDocumentContent`.`version` ".
1715                "LEFT JOIN `tblWorkflowTransitions` ON `tblWorkflowDocumentContent`.`workflow`=`tblWorkflowTransitions`.`workflow` AND `tblWorkflowDocumentContent`.`state`=`tblWorkflowTransitions`.`state` ".
1716                "LEFT JOIN `tblWorkflowTransitionUsers` ON `tblWorkflowTransitionUsers`.`transition` = `tblWorkflowTransitions`.`id` ".
1717                "LEFT JOIN `tblWorkflowTransitionGroups` ON `tblWorkflowTransitionGroups`.`transition` = `tblWorkflowTransitions`.`id` ";
1718
1719            if($user) {
1720                $queryStr .= "WHERE (`tblWorkflowTransitionUsers`.`userid` = ".$user->getID()." ";
1721                if($groups)
1722                    $queryStr .= "OR `tblWorkflowTransitionGroups`.`groupid` IN (".implode(',', $groups).")";
1723                $queryStr .= ") ";
1724            }
1725            $queryStr .= "AND `tblDocumentStatusLog`.`status` = ".S_IN_WORKFLOW." ";
1726//            echo 'SELECT '.$selectStr." ".$queryStr;
1727            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1728            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1729            else $queryStr .= "ORDER BY `name`";
1730            } else {
1731            $queryStr .= "WHERE 1=1 ";
1732            // Get document list for the current user.
1733            $workflowStatus = $user->getWorkflowStatus();
1734
1735            // Create a comma separated list of all the documentIDs whose information is
1736            // required.
1737            $dList = array();
1738            foreach ($workflowStatus["u"] as $st) {
1739                if (!in_array($st["document"], $dList)) {
1740                    $dList[] = $st["document"];
1741                }
1742            }
1743            foreach ($workflowStatus["g"] as $st) {
1744                if (!in_array($st["document"], $dList)) {
1745                    $dList[] = $st["document"];
1746                }
1747            }
1748            $docCSV = "";
1749            foreach ($dList as $d) {
1750                $docCSV .= (strlen($docCSV)==0 ? "" : ", ")."'".$d."'";
1751            }
1752
1753            if (strlen($docCSV)>0) {
1754                $queryStr .=
1755                            //"AND `tblDocumentStatusLog`.`status` IN (".S_IN_WORKFLOW.", ".S_EXPIRED.") ".
1756                            "AND `tblDocuments`.`id` IN (" . $docCSV . ") ".
1757                            "ORDER BY `statusDate` DESC";
1758            } else {
1759                $queryStr = '';
1760            }
1761            }
1762            break; // }}}
1763        case 'AppRevOwner': // Documents waiting for review/approval/revision I'm owning {{{
1764            $queryStr .= "WHERE 1=1 ";
1765
1766            $user = $param1;
1767            $orderby = $param3;
1768            if($param4 == 'desc')
1769                $orderdir = 'DESC';
1770            else
1771                $orderdir = 'ASC';
1772            /** @noinspection PhpUndefinedConstantInspection */
1773            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1774                "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.", ".S_IN_REVISION.") ";
1775            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1776            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1777            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1778            else $queryStr .= "ORDER BY `name`";
1779            $queryStr .= " ".$orderdir;
1780//            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1781//                "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT_REV.", ".S_DRAFT_APP.") ".
1782//                "ORDER BY `statusDate` DESC";
1783            break; // }}}
1784        case 'ReceiveOwner': // Documents having a reception I'm owning {{{
1785            $queryStr .= "WHERE 1=1 ";
1786
1787            $user = $param1;
1788            $orderby = $param3;
1789            if($param4 == 'desc')
1790                $orderdir = 'DESC';
1791            else
1792                $orderdir = 'ASC';
1793
1794            //            $qs = 'SELECT DISTINCT `documentID` FROM `tblDocumentRecipients` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID`';
1795            // sql statement without older versions of a document
1796            $qs = 'SELECT DISTINCT `document` as `documentID` FROM `ttcontentid` a LEFT JOIN `tblDocumentRecipients` b on a.`document`=b.`documentID` AND a.`maxVersion`=b.`version` WHERE b.`receiptID` IS NOT NULL';
1797            $ra = $this->db->getResultArray($qs);
1798            if (is_bool($ra) && !$ra) {
1799                return false;
1800            }
1801            $docs = array();
1802            foreach($ra as $d) {
1803                $docs[] = $d['documentID'];
1804            }
1805
1806            if ($docs) {
1807                $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") ";
1808                $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."'";
1809                $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") ";
1810                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1811                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1812                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1813                else $queryStr .= "ORDER BY `name`";
1814                $queryStr .= " ".$orderdir;
1815            } else {
1816                $queryStr = '';
1817            }
1818            break; // }}}
1819        case 'NoReceiveOwner': // Documents *not* having a reception I'm owning {{{
1820            $queryStr .= "WHERE 1=1 ";
1821
1822            $user = $param1;
1823            $orderby = $param3;
1824            if($param4 == 'desc')
1825                $orderdir = 'DESC';
1826            else
1827                $orderdir = 'ASC';
1828
1829            //            $qs = 'SELECT DISTINCT `documentID` FROM `tblDocumentRecipients` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentRecipients`.`version` AND `ttcontentid`.`document` = `tblDocumentRecipients`.`documentID`';
1830            // sql statement without older versions of a document
1831            $qs = 'SELECT DISTINCT `document` as `documentID` FROM `ttcontentid` a LEFT JOIN `tblDocumentRecipients` b on a.`document`=b.`documentID` AND a.`maxVersion`=b.`version` WHERE b.`receiptID` IS NULL';
1832            $ra = $this->db->getResultArray($qs);
1833            if (is_bool($ra) && !$ra) {
1834                return false;
1835            }
1836            $docs = array();
1837            foreach($ra as $d) {
1838                $docs[] = $d['documentID'];
1839            }
1840
1841            if ($docs) {
1842                $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") ";
1843                $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1844                    "AND `tblDocumentStatusLog`.`status` IN (".S_RELEASED.") ";
1845                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1846                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1847                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1848                else $queryStr .= "ORDER BY `name`";
1849                $queryStr .= " ".$orderdir;
1850            } else {
1851                $queryStr = '';
1852            }
1853            break; // }}}
1854        case 'RejectOwner': // Documents that has been rejected and I'm owning {{{
1855            $queryStr .= "WHERE 1=1 ";
1856
1857            $user = $param1;
1858            $orderby = $param3;
1859            if($param4 == 'desc')
1860                $orderdir = 'DESC';
1861            else
1862                $orderdir = 'ASC';
1863            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
1864            $queryStr .= "AND `tblDocumentStatusLog`.`status` IN (".S_REJECTED.") ";
1865            //$queryStr .= "ORDER BY `statusDate` DESC";
1866            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1867            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1868            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1869            else $queryStr .= "ORDER BY `name`";
1870            $queryStr .= " ".$orderdir;
1871            break; // }}}
1872        case 'LockedByMe': // Documents locked by me {{{
1873            $queryStr .= "WHERE 1=1 ";
1874
1875            $user = $param1;
1876            $orderby = $param3;
1877            if($param4 == 'desc')
1878                $orderdir = 'DESC';
1879            else
1880                $orderdir = 'ASC';
1881
1882            $qs = 'SELECT `document` FROM `tblDocumentLocks` WHERE `userID`='.$user->getID();
1883            $ra = $this->db->getResultArray($qs);
1884            if (is_bool($ra) && !$ra) {
1885                return false;
1886            }
1887            $docs = array();
1888            foreach($ra as $d) {
1889                $docs[] = $d['document'];
1890            }
1891
1892            if ($docs) {
1893                $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") ";
1894                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1895                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1896                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1897                else $queryStr .= "ORDER BY `name`";
1898                $queryStr .= " ".$orderdir;
1899            } else {
1900                $queryStr = '';
1901            }
1902            break; // }}}
1903        case 'ExpiredOwner': // Documents expired and owned by me {{{
1904            if(is_int($param2)) {
1905                $ts = mktime(0, 0, 0) + $param2 * 86400;
1906            } elseif(is_string($param2)) {
1907                $tmp = explode('-', $param2, 3);
1908                if(count($tmp) != 3)
1909                    return false;
1910                if(!self::checkDate($param2, 'Y-m-d'))
1911                    return false;
1912                $ts = mktime(0, 0, 0, (int) $tmp[1], (int) $tmp[2], (int) $tmp[0]);
1913            } else
1914                $ts = mktime(0, 0, 0)-365*86400; /* Start of today - 1 year */
1915
1916            $tsnow = mktime(0, 0, 0); /* Start of today */
1917            if($ts < $tsnow) { /* Check for docs expired in the past */
1918                $startts = $ts;
1919                $endts = $tsnow+86400; /* Use end of day */
1920            } else { /* Check for docs which will expire in the future */
1921                $startts = $tsnow;
1922                $endts = $ts+86400; /* Use end of day */
1923            }
1924
1925            $queryStr .= 
1926                "WHERE `tblDocuments`.`expires` >= ".$startts." AND `tblDocuments`.`expires` <= ".$endts." ";
1927
1928            $user = $param1;
1929            $orderby = $param3;
1930            if($param4 == 'desc')
1931                $orderdir = 'DESC';
1932            else
1933                $orderdir = 'ASC';
1934            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
1935            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1936            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1937            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1938            else $queryStr .= "ORDER BY `name`";
1939            $queryStr .= " ".$orderdir;
1940            break; // }}}
1941        case 'ObsoleteOwner': // Documents that are obsolete and I'm owning {{{
1942            $queryStr .= "WHERE 1=1 ";
1943
1944            $user = $param1;
1945            $orderby = $param3;
1946            if($param4 == 'desc')
1947                $orderdir = 'DESC';
1948            else
1949                $orderdir = 'ASC';
1950            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1951                "AND `tblDocumentStatusLog`.`status` IN (".S_OBSOLETE.") ";
1952            //$queryStr .= "ORDER BY `statusDate` DESC";
1953            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1954            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1955            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1956            else $queryStr .= "ORDER BY `name`";
1957            $queryStr .= " ".$orderdir;
1958            break; // }}}
1959        case 'NeedsCorrectionOwner': // Documents that needs correction and I'm owning {{{
1960            $queryStr .= "WHERE 1=1 ";
1961
1962            $user = $param1;
1963            $orderby = $param3;
1964            if($param4 == 'desc')
1965                $orderdir = 'DESC';
1966            else
1967                $orderdir = 'ASC';
1968            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1969                "AND `tblDocumentStatusLog`.`status` IN (".S_NEEDS_CORRECTION.") ";
1970            //$queryStr .= "ORDER BY `statusDate` DESC";
1971            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1972            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1973            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1974            else $queryStr .= "ORDER BY `name`";
1975            $queryStr .= " ".$orderdir;
1976            break; // }}}
1977        case 'DraftOwner': // Documents in draft status and I'm owning {{{
1978            $queryStr .= "WHERE 1=1 ";
1979
1980            $user = $param1;
1981            $orderby = $param3;
1982            if($param4 == 'desc')
1983                $orderdir = 'DESC';
1984            else
1985                $orderdir = 'ASC';
1986            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
1987                "AND `tblDocumentStatusLog`.`status` IN (".S_DRAFT.") ";
1988            //$queryStr .= "ORDER BY `statusDate` DESC";
1989            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
1990            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
1991            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
1992            else $queryStr .= "ORDER BY `name`";
1993            $queryStr .= " ".$orderdir;
1994            break; // }}}
1995        case 'WorkflowOwner': // Documents waiting for workflow trigger I'm owning {{{
1996            $queryStr .= "WHERE 1=1 ";
1997
1998            $user = $param1;
1999            $queryStr .= "AND `tblDocuments`.`owner` = '".$user->getID()."' ".
2000                "AND `tblDocumentStatusLog`.`status` IN (".S_IN_WORKFLOW.") ".
2001                "ORDER BY `statusDate` DESC";
2002            break; // }}}
2003        case 'MyDocs': // Documents owned by me {{{
2004            $queryStr .= "WHERE 1=1 ";
2005
2006            $user = $param1;
2007            $orderby = $param3;
2008            if($param4 == 'desc')
2009                $orderdir = 'DESC';
2010            else
2011                $orderdir = 'ASC';
2012            $queryStr .=    "AND `tblDocuments`.`owner` = '".$user->getID()."' ";
2013            if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
2014            else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
2015            else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
2016            else $queryStr .= "ORDER BY `name`";
2017            $queryStr .= " ".$orderdir;
2018            break; // }}}
2019        case 'CheckedOutByMe': // Documents I have checked out {{{
2020            $queryStr .= "WHERE 1=1 ";
2021
2022            $user = $param1;
2023            $orderby = $param3;
2024            if($param4 == 'desc')
2025                $orderdir = 'DESC';
2026            else
2027                $orderdir = 'ASC';
2028
2029            $qs = 'SELECT `document` FROM `tblDocumentCheckOuts` WHERE `userID`='.$user->getID();
2030            $ra = $this->db->getResultArray($qs);
2031            if (is_bool($ra) && !$ra) {
2032                return false;
2033            }
2034            $docs = array();
2035            foreach($ra as $d) {
2036                $docs[] = $d['document'];
2037            }
2038
2039            if ($docs) {
2040                $queryStr .= "AND `tblDocuments`.`id` IN (" . implode(',', $docs) . ") ";
2041                if ($orderby=='e') $queryStr .= "ORDER BY `expires`";
2042                else if ($orderby=='u') $queryStr .= "ORDER BY `statusDate`";
2043                else if ($orderby=='s') $queryStr .= "ORDER BY `status`";
2044                else $queryStr .= "ORDER BY `name`";
2045                $queryStr .= " ".$orderdir;
2046            } else {
2047                $queryStr = '';
2048            }
2049            break; // }}}
2050        default: // {{{
2051            return false;
2052            break; // }}}
2053        }
2054
2055        if($queryStr) {
2056            $resArr = $this->db->getResultArray('SELECT '.$selectStr.$queryStr);
2057            if (is_bool($resArr) && !$resArr) {
2058                return false;
2059            }
2060            /*
2061            $documents = array();
2062            foreach($resArr as $row)
2063                $documents[] = $this->getDocument($row["id"]);
2064             */
2065        } else {
2066            return array();
2067        }
2068
2069        return $resArr;
2070    } /* }}} */
2071
2072    /**
2073     * Create a unix time stamp
2074     *
2075     * This method is much like `mktime()` but does some range checks
2076     * on the passed values.
2077     *
2078     * @param int $hour hour
2079     * @param int $min minute
2080     * @param int $sec second
2081     * @param int $year year
2082     * @param int $month month
2083     * @param int $day day
2084     * @return int|boolean unix time stamp or false if range check failed
2085     */
2086    public function makeTimeStamp($hour, $min, $sec, $year, $month, $day) { /* {{{ */
2087        $thirtyone = array (1, 3, 5, 7, 8, 10, 12);
2088        $thirty = array (4, 6, 9, 11);
2089
2090        // Very basic check that the terms are valid. Does not fail for illegal
2091        // dates such as 31 Feb.
2092        if (!is_numeric($hour) || !is_numeric($min) || !is_numeric($sec) || !is_numeric($year) || !is_numeric($month) || !is_numeric($day) || $month<1 || $month>12 || $day<1 || $day>31 || $hour<0 || $hour>23 || $min<0 || $min>59 || $sec<0 || $sec>59) {
2093            return false;
2094        }
2095        $year = (int) $year;
2096        $month = (int) $month;
2097        $day = (int) $day;
2098
2099        if(in_array($month, $thirtyone)) {
2100            $max=31;
2101        } elseif(in_array($month, $thirty)) {
2102            $max=30;
2103        } else {
2104            $max=(($year % 4 == 0) && ($year % 100 != 0 || $year % 400 == 0)) ? 29 : 28;
2105        }
2106
2107        // Check again if day of month is valid in the given month
2108        if ($day>$max) {
2109            return false;
2110        }
2111
2112        return mktime($hour, $min, $sec, $month, $day, $year);
2113    } /* }}} */
2114
2115    /**
2116     * Search the database for documents
2117     *
2118     * Note: the creation date will be used to check againts the
2119     * date saved with the document
2120     * or folder. The modification date will only be used for documents. It
2121     * is checked against the creation date of the document content. This
2122     * meanÑ• that updateÑ• of a document will only result in a searchable
2123     * modification if a new version is uploaded.
2124     *
2125     * If the search is filtered by an expiration date, only documents with
2126     * an expiration date will be found. Even if just an end date is given.
2127     *
2128     * dates, integers and floats fields are treated as ranges (expecting a 'from'
2129     * and 'to' value) unless they have a value set.
2130     *
2131     * @param string $query seach query with space separated words
2132     * @param integer $limit number of items in result set
2133     * @param integer $offset index of first item in result set
2134     * @param string $logicalmode either AND or OR
2135     * @param array $searchin list of fields to search in
2136     *        1 = keywords, 2=name, 3=comment, 4=attributes, 5=id
2137     * @param SeedDMS_Core_Folder|null $startFolder search in the folder only (null for root folder)
2138     * @param SeedDMS_Core_User $owner search for documents owned by this user
2139     * @param array $status list of status
2140     * @param array $creationstartdate search for documents created after this date
2141     * @param array $creationenddate search for documents created before this date
2142     * @param array $modificationstartdate search for documents modified after this date
2143     * @param array $modificationenddate search for documents modified before this date
2144     * @param array $categories list of categories the documents must have assigned
2145     * @param array $attributes list of attributes. The key of this array is the
2146     * attribute definition id. The value of the array is the value of the
2147     * attribute. If the attribute may have multiple values it must be an array.
2148     * attributes with a range must have the elements 'from' and 'to'
2149     * @param integer $mode decide whether to search for documents/folders
2150     *        0x1 = documents only
2151     *        0x2 = folders only
2152     *        0x3 = both
2153     * @param array $expirationstartdate search for documents expiring after and on this date
2154     * @param array $expirationenddate search for documents expiring before and on this date
2155     * @return array|bool
2156     */
2157    function search($query, $limit=0, $offset=0, $logicalmode='AND', $searchin=array(), $startFolder=null, $owner=null, $status = array(), $creationstartdate=array(), $creationenddate=array(), $modificationstartdate=array(), $modificationenddate=array(), $categories=array(), $attributes=array(), $mode=0x3, $expirationstartdate=array(), $expirationenddate=array(), $reception=array()) { /* {{{ */
2158        $orderby = '';
2159        $revisionstartdate = $revisionenddate = '';
2160        $statusstartdate = array();
2161        $statusenddate = array();
2162        if(is_array($query)) {
2163            foreach(array('limit', 'offset', 'logicalmode', 'searchin', 'startFolder', 'owner', 'status', 'creationstartdate', 'creationenddate', 'modificationstartdate', 'modificationenddate', 'categories', 'attributes', 'mode', 'revisionstartdate', 'revisionenddate', 'expirationstartdate', 'expirationenddate', 'reception') as $paramname)
2164                ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : ${$paramname};
2165            foreach(array('orderby', 'statusstartdate', 'statusenddate') as $paramname)
2166                ${$paramname} = isset($query[$paramname]) ? $query[$paramname] : '';
2167            $query = isset($query['query']) ? $query['query'] : '';
2168        }
2169        /* Ensure $logicalmode has a valid value */
2170        if($logicalmode != 'OR')
2171            $logicalmode = 'AND';
2172
2173        // Split the search string into constituent keywords.
2174        $tkeys=array();
2175        if (strlen($query)>0) {
2176            $tkeys = preg_split("/[\t\r\n ,]+/", $query);
2177        }
2178
2179        // if none is checkd search all
2180        if (count($searchin)==0)
2181            $searchin=array(1, 2, 3, 4, 5);
2182
2183        /*--------- Do it all over again for folders -------------*/
2184        $totalFolders = 0;
2185        if($mode & 0x2) {
2186            $searchKey = "";
2187
2188            $classname = $this->classnames['folder'];
2189            $searchFields = $classname::getSearchFields($this, $searchin);
2190
2191            if (count($searchFields)>0) {
2192                foreach ($tkeys as $key) {
2193                    $key = trim($key);
2194                    if (strlen($key)>0) {
2195                        $searchKey = (strlen($searchKey)==0 ? "" : $searchKey." ".$logicalmode." ")."(".implode(" like ".$this->db->qstr("%".$key."%")." OR ", $searchFields)." like ".$this->db->qstr("%".$key."%").")";
2196                    }
2197                }
2198            }
2199
2200            // Check to see if the search has been restricted to a particular sub-tree in
2201            // the folder hierarchy.
2202            $searchFolder = "";
2203            if ($startFolder) {
2204                $searchFolder = "`tblFolders`.`folderList` LIKE '%:".$startFolder->getID().":%'";
2205                if($this->checkWithinRootDir)
2206                    $searchFolder = '('.$searchFolder." AND `tblFolders`.`folderList` LIKE '%:".$this->rootFolderID.":%')";
2207            } elseif($this->checkWithinRootDir) {
2208                $searchFolder = "`tblFolders`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
2209            }
2210
2211            // Check to see if the search has been restricted to a particular
2212            // document owner.
2213            $searchOwner = "";
2214            if ($owner) {
2215                if(is_array($owner)) {
2216                    $ownerids = array();
2217                    foreach($owner as $o)
2218                        $ownerids[] = $o->getID();
2219                    if($ownerids)
2220                        $searchOwner = "`tblFolders`.`owner` IN (".implode(',', $ownerids).")";
2221                } else {
2222                    $searchOwner = "`tblFolders`.`owner` = '".$owner->getId()."'";
2223                }
2224            }
2225
2226            // Check to see if the search has been restricted to a particular
2227            // attribute.
2228            $searchAttributes = array();
2229            if ($attributes) {
2230                foreach($attributes as $attrdefid=>$attribute) {
2231                    if($attribute) {
2232                        $attrdef = $this->getAttributeDefinition($attrdefid);
2233                        if($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_folder || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
2234                            if($valueset = $attrdef->getValueSet()) {
2235                                if(is_string($attribute))
2236                                    $attribute = array($attribute);
2237                                foreach($attribute as &$v)
2238                                    $v = trim($this->db->qstr($v), "'");
2239                                if($attrdef->getMultipleValues()) {
2240                                    $searchAttributes[] = "EXISTS (SELECT NULL FROM `tblFolderAttributes` WHERE `tblFolderAttributes`.`attrdef`=".$attrdefid." AND (`tblFolderAttributes`.`value` like '%".$valueset[0].implode("%' OR `tblFolderAttributes`.`value` like '%".$valueset[0], $attribute)."%') AND `tblFolderAttributes`.`folder`=`tblFolders`.`id`)";
2241                                } else {
2242                                    $searchAttributes[] = "EXISTS (SELECT NULL FROM `tblFolderAttributes` WHERE `tblFolderAttributes`.`attrdef`=".$attrdefid." AND (`tblFolderAttributes`.`value`='".(is_array($attribute) ? implode("' OR `tblFolderAttributes`.`value` = '", $attribute) : $attribute)."') AND `tblFolderAttributes`.`folder`=`tblFolders`.`id`)";
2243                                }
2244                            } else {
2245                                if(in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_date, SeedDMS_Core_AttributeDefinition::type_int, SeedDMS_Core_AttributeDefinition::type_float]) && is_array($attribute)) {
2246                                    $kkll = [];
2247                                    if(!empty($attribute['from'])) {
2248                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2249                                            $kkll[] = "CAST(`tblFolderAttributes`.`value` AS INTEGER)>=".(int) $attribute['from'];
2250                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2251                                            $kkll[] = "CAST(`tblFolderAttributes`.`value` AS DECIMAL)>=".(float) $attribute['from'];
2252                                        else
2253                                            $kkll[] = "`tblFolderAttributes`.`value`>=".$this->db->qstr($attribute['from']);
2254                                    }
2255                                    if(!empty($attribute['to'])) {
2256                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2257                                            $kkll[] = "CAST(`tblFolderAttributes`.`value` AS INTEGER)<=".(int) $attribute['to'];
2258                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2259                                            $kkll[] = "CAST(`tblFolderAttributes`.`value` AS DECIMAL)<=".(float) $attribute['to'];
2260                                        else
2261                                            $kkll[] = "`tblFolderAttributes`.`value`<=".$this->db->qstr($attribute['to']);
2262                                    }
2263                                    if($kkll)
2264                                        $searchAttributes[] = "EXISTS (SELECT NULL FROM `tblFolderAttributes` WHERE `tblFolderAttributes`.`attrdef`=".$attrdefid." AND ".implode(' AND ', $kkll)." AND `tblFolderAttributes`.`folder`=`tblFolders`.`id`)";
2265                                } elseif(is_string($attribute)) {
2266                                    $searchAttributes[] = "EXISTS (SELECT NULL FROM `tblFolderAttributes` WHERE `tblFolderAttributes`.`attrdef`=".$attrdefid." AND `tblFolderAttributes`.`value` like ".$this->db->qstr("%".$attribute."%")." AND `tblFolderAttributes`.`folder`=`tblFolders`.`id`)";
2267                                }
2268                            }
2269                        }
2270                    }
2271                }
2272            }
2273
2274            // Is the search restricted to documents created between two specific dates?
2275            $searchCreateDate = "";
2276            if ($creationstartdate) {
2277                if(is_numeric($creationstartdate))
2278                    $startdate = $creationstartdate;
2279                else
2280                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
2281                if ($startdate) {
2282                    $searchCreateDate .= "`tblFolders`.`date` >= ".(int) $startdate;
2283                }
2284            }
2285            if ($creationenddate) {
2286                if(is_numeric($creationenddate))
2287                    $stopdate = $creationenddate;
2288                else
2289                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
2290                if ($stopdate) {
2291                    /** @noinspection PhpUndefinedVariableInspection */
2292                    if($startdate)
2293                        $searchCreateDate .= " AND ";
2294                    $searchCreateDate .= "`tblFolders`.`date` <= ".(int) $stopdate;
2295                }
2296            }
2297
2298            $searchQuery = "FROM ".$classname::getSearchTables()." WHERE 1=1";
2299
2300            if (strlen($searchKey)>0) {
2301                $searchQuery .= " AND (".$searchKey.")";
2302            }
2303            if (strlen($searchFolder)>0) {
2304                $searchQuery .= " AND ".$searchFolder;
2305            }
2306            if (strlen($searchOwner)>0) {
2307                $searchQuery .= " AND (".$searchOwner.")";
2308            }
2309            if (strlen($searchCreateDate)>0) {
2310                $searchQuery .= " AND (".$searchCreateDate.")";
2311            }
2312            if ($searchAttributes) {
2313                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
2314            }
2315
2316            /* Do not search for folders if not at least a search for a key,
2317             * an owner, or creation date is requested.
2318             */
2319            if($searchKey || $searchOwner || $searchCreateDate || $searchAttributes) {
2320                // Count the number of rows that the search will produce.
2321                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblFolders`.id ".$searchQuery.") a");
2322                if ($resArr && isset($resArr[0]) && is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
2323                    $totalFolders = (integer)$resArr[0]["num"];
2324                }
2325
2326                // If there are no results from the count query, then there is no real need
2327                // to run the full query. TODO: re-structure code to by-pass additional
2328                // queries when no initial results are found.
2329
2330                // Only search if the offset is not beyond the number of folders
2331                if($totalFolders > $offset) {
2332                    // Prepare the complete search query, including the LIMIT clause.
2333                    $searchQuery = "SELECT DISTINCT `tblFolders`.`id` ".$searchQuery." GROUP BY `tblFolders`.`id`";
2334
2335                    switch($orderby) {
2336                    case 'dd':
2337                        $searchQuery .= " ORDER BY `tblFolders`.`date` DESC";
2338                        break;
2339                    case 'da':
2340                    case 'd':
2341                        $searchQuery .= " ORDER BY `tblFolders`.`date`";
2342                        break;
2343                    case 'nd':
2344                        $searchQuery .= " ORDER BY `tblFolders`.`name` DESC";
2345                        break;
2346                    case 'na':
2347                    case 'n':
2348                        $searchQuery .= " ORDER BY `tblFolders`.`name`";
2349                        break;
2350                    case 'id':
2351                        $searchQuery .= " ORDER BY `tblFolders`.`id` DESC";
2352                        break;
2353                    case 'ia':
2354                    case 'i':
2355                        $searchQuery .= " ORDER BY `tblFolders`.`id`";
2356                        break;
2357                    default:
2358                        break;
2359                    }
2360
2361                    if($limit) {
2362                        $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
2363                    }
2364
2365                    // Send the complete search query to the database.
2366                    $resArr = $this->db->getResultArray($searchQuery);
2367                } else {
2368                    $resArr = array();
2369                }
2370
2371                // ------------------- Ausgabe der Ergebnisse ----------------------------
2372                $numResults = count($resArr);
2373                if ($numResults == 0) {
2374                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>array());
2375                } else {
2376                    foreach ($resArr as $folderArr) {
2377                        $folders[] = $this->getFolder($folderArr['id']);
2378                    }
2379                    /** @noinspection PhpUndefinedVariableInspection */
2380                    $folderresult = array('totalFolders'=>$totalFolders, 'folders'=>$folders);
2381                }
2382            } else {
2383                $folderresult = array('totalFolders'=>0, 'folders'=>array());
2384            }
2385        } else {
2386            $folderresult = array('totalFolders'=>0, 'folders'=>array());
2387        }
2388
2389        /*--------- Do it all over again for documents -------------*/
2390
2391        $totalDocs = 0;
2392        if($mode & 0x1) {
2393            $searchKey = "";
2394
2395            $classname = $this->classnames['document'];
2396            $searchFields = $classname::getSearchFields($this, $searchin);
2397
2398            if (count($searchFields)>0) {
2399                foreach ($tkeys as $key) {
2400                    $key = trim($key);
2401                    if (strlen($key)>0) {
2402                        $searchKey = (strlen($searchKey)==0 ? "" : $searchKey." ".$logicalmode." ")."(".implode(" like ".$this->db->qstr("%".$key."%")." OR ", $searchFields)." like ".$this->db->qstr("%".$key."%").")";
2403                    }
2404                }
2405            }
2406
2407            // Check to see if the search has been restricted to a particular sub-tree in
2408            // the folder hierarchy.
2409            $searchFolder = "";
2410            if ($startFolder) {
2411                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$startFolder->getID().":%'";
2412                if($this->checkWithinRootDir)
2413                    $searchFolder = '('.$searchFolder." AND `tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%')";
2414            } elseif($this->checkWithinRootDir) {
2415                $searchFolder = "`tblDocuments`.`folderList` LIKE '%:".$this->rootFolderID.":%'";
2416            }
2417
2418            // Check to see if the search has been restricted to a particular
2419            // document owner.
2420            $searchOwner = "";
2421            if ($owner) {
2422                if(is_array($owner)) {
2423                    $ownerids = array();
2424                    foreach($owner as $o)
2425                        $ownerids[] = $o->getID();
2426                    if($ownerids)
2427                        $searchOwner = "`tblDocuments`.`owner` IN (".implode(',', $ownerids).")";
2428                } else {
2429                    $searchOwner = "`tblDocuments`.`owner` = '".$owner->getId()."'";
2430                }
2431            }
2432
2433            // Check to see if the search has been restricted to a particular
2434            // document category.
2435            $searchCategories = "";
2436            if ($categories) {
2437                $catids = array();
2438                foreach($categories as $category)
2439                    $catids[] = $category->getId();
2440                $searchCategories = "`tblDocumentCategory`.`categoryID` in (".implode(',', $catids).")";
2441            }
2442
2443            // Check to see if the search has been restricted to a particular
2444            // attribute.
2445            $searchAttributes = array();
2446            if ($attributes) {
2447                foreach($attributes as $attrdefid=>$attribute) {
2448                    if($attribute) {
2449                        $lsearchAttributes = [];
2450                        $attrdef = $this->getAttributeDefinition($attrdefid);
2451                        if($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_document || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
2452                            if($valueset = $attrdef->getValueSet()) {
2453                                if(is_string($attribute))
2454                                    $attribute = array($attribute);
2455                                foreach($attribute as &$v)
2456                                    $v = trim($this->db->qstr($v), "'");
2457                                if($attrdef->getMultipleValues()) {
2458                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentAttributes` WHERE `tblDocumentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentAttributes`.`value` like '%".$valueset[0].implode("%' OR `tblDocumentAttributes`.`value` like '%".$valueset[0], $attribute)."%') AND `tblDocumentAttributes`.`document` = `tblDocuments`.`id`)";
2459                                } else {
2460                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentAttributes` WHERE `tblDocumentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentAttributes`.`value`='".(is_array($attribute) ? implode("' OR `tblDocumentAttributes`.`value` = '", $attribute) : $attribute)."') AND `tblDocumentAttributes`.`document` = `tblDocuments`.`id`)";
2461                                }
2462                            } else {
2463                                if(in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_date, SeedDMS_Core_AttributeDefinition::type_int, SeedDMS_Core_AttributeDefinition::type_float]) && is_array($attribute)) {
2464                                    $kkll = [];
2465                                    if(!empty($attribute['from'])) {
2466                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2467                                            $kkll[] = "CAST(`tblDocumentAttributes`.`value` AS INTEGER)>=".(int) $attribute['from'];
2468                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2469                                            $kkll[] = "CAST(`tblDocumentAttributes`.`value` AS DECIMAL)>=".(float) $attribute['from'];
2470                                        else
2471                                            $kkll[] = "`tblDocumentAttributes`.`value`>=".$this->db->qstr($attribute['from']);
2472                                    }
2473                                    if(!empty($attribute['to'])) {
2474                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2475                                            $kkll[] = "CAST(`tblDocumentAttributes`.`value` AS INTEGER)<=".(int) $attribute['to'];
2476                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2477                                            $kkll[] = "CAST(`tblDocumentAttributes`.`value` AS DECIMAL)<=".(float) $attribute['to'];
2478                                        else
2479                                            $kkll[] = "`tblDocumentAttributes`.`value`<=".$this->db->qstr($attribute['to']);
2480                                    }
2481                                    if($kkll)
2482                                        $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentAttributes` WHERE `tblDocumentAttributes`.`attrdef`=".$attrdefid." AND ".implode(' AND ', $kkll)." AND `tblDocumentAttributes`.`document`=`tblDocuments`.`id`)";
2483                                } else {
2484                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentAttributes` WHERE `tblDocumentAttributes`.`attrdef`=".$attrdefid." AND `tblDocumentAttributes`.`value` like ".$this->db->qstr("%".$attribute."%")." AND `tblDocumentAttributes`.`document` = `tblDocuments`.`id`)";
2485                                }
2486                            }
2487                        }
2488                        if($attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_documentcontent || $attrdef->getObjType() == SeedDMS_Core_AttributeDefinition::objtype_all) {
2489                            if($valueset = $attrdef->getValueSet()) {
2490                                if(is_string($attribute))
2491                                    $attribute = array($attribute);
2492                                foreach($attribute as &$v)
2493                                    $v = trim($this->db->qstr($v), "'");
2494                                if($attrdef->getMultipleValues()) {
2495                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentContentAttributes`.`value` like '%".$valueset[0].implode("%' OR `tblDocumentContentAttributes`.`value` like '%".$valueset[0], $attribute)."%') AND `tblDocumentContentAttributes`.`content` = `tblDocumentContent`.`id`)";
2496                                } else {
2497                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND (`tblDocumentContentAttributes`.`value`='".(is_array($attribute) ? implode("' OR `tblDocumentContentAttributes`.`value` = '", $attribute) : $attribute)."') AND `tblDocumentContentAttributes`.content = `tblDocumentContent`.id)";
2498                                }
2499                            } else {
2500                                if(in_array($attrdef->getType(), [SeedDMS_Core_AttributeDefinition::type_date, SeedDMS_Core_AttributeDefinition::type_int, SeedDMS_Core_AttributeDefinition::type_float]) && is_array($attribute)) {
2501                                    $kkll = [];
2502                                    if(!empty($attribute['from'])) {
2503                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2504                                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS INTEGER)>=".(int) $attribute['from'];
2505                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2506                                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS DECIMAL)>=".(float) $attribute['from'];
2507                                        else
2508                                            $kkll[] = "`tblDocumentContentAttributes`.`value`>=".$this->db->qstr($attribute['from']);
2509                                    }
2510                                    if(!empty($attribute['to'])) {
2511                                        if($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_int)
2512                                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS INTEGER)<=".(int) $attribute['to'];
2513                                        elseif($attrdef->getType() == SeedDMS_Core_AttributeDefinition::type_float)
2514                                            $kkll[] = "CAST(`tblDocumentContentAttributes`.`value` AS DECIMAL)<=".(float) $attribute['to'];
2515                                        else
2516                                            $kkll[] = "`tblDocumentContentAttributes`.`value`<=".$this->db->qstr($attribute['to']);
2517                                    }
2518                                    if($kkll)
2519                                        $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND ".implode(' AND ', $kkll)." AND `tblDocumentContentAttributes`.`content`=`tblDocumentContent`.`id`)";
2520                                } else {
2521                                    $lsearchAttributes[] = "EXISTS (SELECT NULL FROM `tblDocumentContentAttributes` WHERE `tblDocumentContentAttributes`.`attrdef`=".$attrdefid." AND `tblDocumentContentAttributes`.`value` like ".$this->db->qstr("%".$attribute."%")." AND `tblDocumentContentAttributes`.content = `tblDocumentContent`.id)";
2522                                }
2523                            }
2524                        }
2525                        if($lsearchAttributes)
2526                            $searchAttributes[] = "(".implode(" OR ", $lsearchAttributes).")";
2527                    }
2528                }
2529            }
2530
2531            // Is the search restricted to documents created between two specific dates?
2532            $searchCreateDate = "";
2533            if ($creationstartdate) {
2534                if(is_numeric($creationstartdate))
2535                    $startdate = $creationstartdate;
2536                else
2537                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($creationstartdate['hour'], $creationstartdate['minute'], $creationstartdate['second'], $creationstartdate['year'], $creationstartdate["month"], $creationstartdate["day"]);
2538                if ($startdate) {
2539                    $searchCreateDate .= "`tblDocuments`.`date` >= ".(int) $startdate;
2540                }
2541            }
2542            if ($creationenddate) {
2543                if(is_numeric($creationenddate))
2544                    $stopdate = $creationenddate;
2545                else
2546                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($creationenddate['hour'], $creationenddate['minute'], $creationenddate['second'], $creationenddate["year"], $creationenddate["month"], $creationenddate["day"]);
2547                if ($stopdate) {
2548                    if($searchCreateDate)
2549                        $searchCreateDate .= " AND ";
2550                    $searchCreateDate .= "`tblDocuments`.`date` <= ".(int) $stopdate;
2551                }
2552            }
2553
2554            if ($modificationstartdate) {
2555                if(is_numeric($modificationstartdate))
2556                    $startdate = $modificationstartdate;
2557                else
2558                    $startdate = SeedDMS_Core_DMS::makeTimeStamp($modificationstartdate['hour'], $modificationstartdate['minute'], $modificationstartdate['second'], $modificationstartdate['year'], $modificationstartdate["month"], $modificationstartdate["day"]);
2559                if ($startdate) {
2560                    if($searchCreateDate)
2561                        $searchCreateDate .= " AND ";
2562                    $searchCreateDate .= "`tblDocumentContent`.`date` >= ".(int) $startdate;
2563                }
2564            }
2565            if ($modificationenddate) {
2566                if(is_numeric($modificationenddate))
2567                    $stopdate = $modificationenddate;
2568                else
2569                    $stopdate = SeedDMS_Core_DMS::makeTimeStamp($modificationenddate['hour'], $modificationenddate['minute'], $modificationenddate['second'], $modificationenddate["year"], $modificationenddate["month"], $modificationenddate["day"]);
2570                if ($stopdate) {
2571                    if($searchCreateDate)
2572                        $searchCreateDate .= " AND ";
2573                    $searchCreateDate .= "`tblDocumentContent`.`date` <= ".(int) $stopdate;
2574                }
2575            }
2576            $searchRevisionDate = "";
2577            if ($revisionstartdate) {
2578                $startdate = sprintf('%04d-%02d-%02d', $revisionstartdate['year'], $revisionstartdate["month"], $revisionstartdate["day"]);
2579                if ($startdate) {
2580                    if($searchRevisionDate)
2581                        $searchRevisionDate .= " AND ";
2582                    $searchRevisionDate .= "`tblDocumentContent`.`revisiondate` >= '".$startdate."'";
2583                }
2584            }
2585            if ($revisionenddate) {
2586                $stopdate = sprintf('%04d-%02d-%02d', $revisionenddate["year"], $revisionenddate["month"], $revisionenddate["day"]);
2587                if ($stopdate) {
2588                    if($searchRevisionDate)
2589                        $searchRevisionDate .= " AND ";
2590                    $searchRevisionDate .= "`tblDocumentContent`.`revisiondate` <= '".$stopdate."'";
2591                }
2592            }
2593            $searchExpirationDate = '';
2594            if ($expirationstartdate) {
2595                $startdate = SeedDMS_Core_DMS::makeTimeStamp($expirationstartdate['hour'], $expirationstartdate['minute'], $expirationstartdate['second'], $expirationstartdate['year'], $expirationstartdate["month"], $expirationstartdate["day"]);
2596                if ($startdate) {
2597                    $searchExpirationDate .= "`tblDocuments`.`expires` >= ".(int) $startdate;
2598                }
2599            }
2600            if ($expirationenddate) {
2601                $stopdate = SeedDMS_Core_DMS::makeTimeStamp($expirationenddate['hour'], $expirationenddate['minute'], $expirationenddate['second'], $expirationenddate["year"], $expirationenddate["month"], $expirationenddate["day"]);
2602                if ($stopdate) {
2603                    if($searchExpirationDate)
2604                        $searchExpirationDate .= " AND ";
2605                    else // do not find documents without an expiration date
2606                        $searchExpirationDate .= "`tblDocuments`.`expires` != 0 AND ";
2607                    $searchExpirationDate .= "`tblDocuments`.`expires` <= ".(int) $stopdate;
2608                }
2609            }
2610            $searchStatusDate = '';
2611            if ($statusstartdate) {
2612                $startdate = $statusstartdate['year'].'-'.$statusstartdate["month"].'-'.$statusstartdate["day"].' '.$statusstartdate['hour'].':'.$statusstartdate['minute'].':'.$statusstartdate['second'];
2613                if ($startdate) {
2614                    if($searchStatusDate)
2615                        $searchStatusDate .= " AND ";
2616                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` >= ".$this->db->qstr($startdate);
2617                }
2618            }
2619            if ($statusenddate) {
2620                $stopdate = $statusenddate['year'].'-'.$statusenddate["month"].'-'.$statusenddate["day"].' '.$statusenddate['hour'].':'.$statusenddate['minute'].':'.$statusenddate['second'];
2621                if ($stopdate) {
2622                    if($searchStatusDate)
2623                        $searchStatusDate .= " AND ";
2624                    $searchStatusDate .= "`tblDocumentStatusLog`.`date` <= ".$this->db->qstr($stopdate);
2625                }
2626            }
2627
2628            // ---------------------- Suche starten ----------------------------------
2629
2630            //
2631            // Construct the SQL query that will be used to search the database.
2632            //
2633
2634            if (!$this->db->createTemporaryTable("ttcontentid") || !$this->db->createTemporaryTable("ttstatid")) {
2635                return false;
2636            }
2637            if($reception) {
2638                if (!$this->db->createTemporaryTable("ttreceiptid")) {
2639                    return false;
2640                }
2641            }
2642
2643            $searchQuery = "FROM `tblDocuments` ".
2644                "LEFT JOIN `tblDocumentContent` ON `tblDocuments`.`id` = `tblDocumentContent`.`document` ".
2645                "LEFT JOIN `tblDocumentAttributes` ON `tblDocuments`.`id` = `tblDocumentAttributes`.`document` ".
2646                "LEFT JOIN `tblDocumentContentAttributes` ON `tblDocumentContent`.`id` = `tblDocumentContentAttributes`.`content` ".
2647                "LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatus`.`documentID` = `tblDocumentContent`.`document` ".
2648                "LEFT JOIN `ttstatid` ON `ttstatid`.`statusID` = `tblDocumentStatus`.`statusID` ".
2649                "LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatusLog`.`statusLogID` = `ttstatid`.`maxLogID` ".
2650                "LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion` = `tblDocumentStatus`.`version` AND `ttcontentid`.`document` = `tblDocumentStatus`.`documentID` ".
2651                "LEFT JOIN `tblDocumentLocks` ON `tblDocuments`.`id`=`tblDocumentLocks`.`document` ".
2652                "LEFT JOIN `tblDocumentCategory` ON `tblDocuments`.`id`=`tblDocumentCategory`.`documentID` ".
2653//                "LEFT JOIN `tblDocumentRecipients` ON `tblDocuments`.`id`=`tblDocumentRecipients`.`documentID` ".
2654//                "LEFT JOIN `tblDocumentReceiptLog` ON `tblDocumentRecipients`.`receiptID`=`tblDocumentReceiptLog`.`receiptID` ".
2655//                "LEFT JOIN `ttreceiptid` ON `ttreceiptid`.`maxLogID` = `tblDocumentReceiptLog`.`receiptLogID` ".
2656                "WHERE ".
2657//                "`ttstatid`.`maxLogID`=`tblDocumentStatusLog`.`statusLogID` AND ".
2658//                "`ttreceiptid`.`maxLogID`=`tblDocumentReceiptLog`.`receiptLogID` AND ".
2659                "`ttcontentid`.`maxVersion` = `tblDocumentContent`.`version`";
2660
2661            if (strlen($searchKey)>0) {
2662                $searchQuery .= " AND (".$searchKey.")";
2663            }
2664            if (strlen($searchFolder)>0) {
2665                $searchQuery .= " AND ".$searchFolder;
2666            }
2667            if (strlen($searchOwner)>0) {
2668                $searchQuery .= " AND (".$searchOwner.")";
2669            }
2670            if (strlen($searchCategories)>0) {
2671                $searchQuery .= " AND (".$searchCategories.")";
2672            }
2673            if (strlen($searchCreateDate)>0) {
2674                $searchQuery .= " AND (".$searchCreateDate.")";
2675            }
2676            if (strlen($searchRevisionDate)>0) {
2677                $searchQuery .= " AND (".$searchRevisionDate.")";
2678            }
2679            if (strlen($searchExpirationDate)>0) {
2680                $searchQuery .= " AND (".$searchExpirationDate.")";
2681            }
2682            if (strlen($searchStatusDate)>0) {
2683                $searchQuery .= " AND (".$searchStatusDate.")";
2684            }
2685            if ($searchAttributes) {
2686                $searchQuery .= " AND (".implode(" AND ", $searchAttributes).")";
2687            }
2688
2689            // status
2690            if ($status) {
2691                $searchQuery .= " AND `tblDocumentStatusLog`.`status` IN (".implode(',', $status).")";
2692            }
2693
2694            if($reception) {
2695                $searchReception = array();
2696                /* still waiting for users/groups to acknownledge reception */
2697                if(in_array("missingaction", $reception))
2698                    $searchReception[] = "b.`status` IN (0)";
2699                /* document has not been acknowledeged by at least one user/group */
2700                if(in_array("hasrejection", $reception))
2701                    $searchReception[] = "b.`status` IN (-1)";
2702                /* document has been acknowledeged by at least one user/group */
2703                if(in_array("hasacknowledge", $reception))
2704                    $searchReception[] = "b.`status` IN (1)";
2705                /* document has been acknowledeged by all users/groups !!! not working !!! */
2706                if(in_array("completeacknowledge", $reception))
2707                    $searchReception[] = "b.`status` NOT IN (-1, 0)";
2708                if($searchReception) {
2709                    $searchQuery .= " AND EXISTS (SELECT NULL FROM `tblDocumentRecipients` a LEFT JOIN `tblDocumentReceiptLog` b ON a.`receiptID`=b.`receiptID` LEFT JOIN `ttreceiptid` c ON c.`maxLogID` = b.`receiptLogID` WHERE ";
2710                    $searchQuery .= "c.`maxLogID`=b.`receiptLogID` AND `tblDocuments`.`id` = a.`documentID` ";
2711                    $searchQuery .= "AND (".implode(' OR ', $searchReception)."))";
2712                }
2713            }
2714
2715            if($searchKey || $searchOwner || $searchCategories || $searchCreateDate || $searchRevisionDate || $searchExpirationDate || $searchStatusDate || $searchAttributes || $status) {
2716                // Count the number of rows that the search will produce.
2717                $resArr = $this->db->getResultArray("SELECT COUNT(*) AS num FROM (SELECT DISTINCT `tblDocuments`.`id` ".$searchQuery.") a");
2718                $totalDocs = 0;
2719                if (is_numeric($resArr[0]["num"]) && $resArr[0]["num"]>0) {
2720                    $totalDocs = (integer)$resArr[0]["num"];
2721                }
2722
2723                // If there are no results from the count query, then there is no real need
2724                // to run the full query. TODO: re-structure code to by-pass additional
2725                // queries when no initial results are found.
2726
2727                // Prepare the complete search query, including the LIMIT clause.
2728                $searchQuery = "SELECT DISTINCT `tblDocuments`.*, ".
2729                    "`tblDocumentContent`.`version`, ".
2730                    "`tblDocumentStatusLog`.`status`, `tblDocumentLocks`.`userID` as `lockUser` ".$searchQuery;
2731
2732                switch($orderby) {
2733                case 'dd':
2734                    $orderbyQuery = " ORDER BY `tblDocuments`.`date` DESC";
2735                    break;
2736                case 'da':
2737                case 'd':
2738                    $orderbyQuery = " ORDER BY `tblDocuments`.`date`";
2739                    break;
2740                case 'nd':
2741                    $orderbyQuery = " ORDER BY `tblDocuments`.`name` DESC";
2742                    break;
2743                case 'na':
2744                case 'n':
2745                    $orderbyQuery = " ORDER BY `tblDocuments`.`name`";
2746                    break;
2747                case 'id':
2748                    $orderbyQuery = " ORDER BY `tblDocuments`.`id` DESC";
2749                    break;
2750                case 'ia':
2751                case 'i':
2752                    $orderbyQuery = " ORDER BY `tblDocuments`.`id`";
2753                    break;
2754                default:
2755                    $orderbyQuery = "";
2756                    break;
2757                }
2758
2759                // calculate the remaining entrÑ—es of the current page
2760                // If page is not full yet, get remaining entries
2761                if($limit) {
2762                    $remain = $limit - count($folderresult['folders']);
2763                    if($remain) {
2764                        if($remain == $limit)
2765                            $offset -= $totalFolders;
2766                        else
2767                            $offset = 0;
2768
2769                        $searchQuery .= $orderbyQuery;
2770
2771                        if($limit)
2772                            $searchQuery .= " LIMIT ".$limit." OFFSET ".$offset;
2773
2774                        // Send the complete search query to the database.
2775                        $resArr = $this->db->getResultArray($searchQuery);
2776                        if($resArr === false)
2777                            return false;
2778                    } else {
2779                        $resArr = array();
2780                    }
2781                } else {
2782                    $searchQuery .= $orderbyQuery;
2783
2784                    // Send the complete search query to the database.
2785                    $resArr = $this->db->getResultArray($searchQuery);
2786                    if($resArr === false)
2787                        return false;
2788                }
2789
2790                // ------------------- Ausgabe der Ergebnisse ----------------------------
2791                $numResults = count($resArr);
2792                if ($numResults == 0) {
2793                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>array());
2794                } else {
2795                    foreach ($resArr as $docArr) {
2796                        $docs[] = $this->getDocument($docArr['id']);
2797                    }
2798                    /** @noinspection PhpUndefinedVariableInspection */
2799                    $docresult = array('totalDocs'=>$totalDocs, 'docs'=>$docs);
2800                }
2801            } else {
2802                $docresult = array('totalDocs'=>0, 'docs'=>array());
2803            }
2804        } else {
2805            $docresult = array('totalDocs'=>0, 'docs'=>array());
2806        }
2807
2808        if($limit) {
2809            $totalPages = (integer)(($totalDocs+$totalFolders)/$limit);
2810            if ((($totalDocs+$totalFolders)%$limit) > 0) {
2811                $totalPages++;
2812            }
2813        } else {
2814            $totalPages = 1;
2815        }
2816
2817        return array_merge($docresult, $folderresult, array('totalPages'=>$totalPages));
2818    } /* }}} */
2819
2820    /**
2821     * Return a folder by its id
2822     *
2823     * This method retrieves a folder from the database by its id.
2824     *
2825     * @param integer $id internal id of folder
2826     * @return SeedDMS_Core_Folder instance of SeedDMS_Core_Folder or false
2827     */
2828    function getFolder($id) { /* {{{ */
2829        $classname = $this->classnames['folder'];
2830        return $classname::getInstance($id, $this);
2831    } /* }}} */
2832
2833    /**
2834     * Return a folder by its name
2835     *
2836     * This method retrieves a folder from the database by its name. The
2837     * search covers the whole database. If
2838     * the parameter $folder is not null, it will search for the name
2839     * only within this parent folder. It will not be done recursively.
2840     *
2841     * @param string $name name of the folder
2842     * @param SeedDMS_Core_Folder $folder parent folder
2843     * @return SeedDMS_Core_Folder|boolean found folder or false
2844     */
2845    function getFolderByName($name, $folder=null) { /* {{{ */
2846        $name = trim($name);
2847        $classname = $this->classnames['folder'];
2848        return $classname::getInstanceByName($name, $folder, $this);
2849    } /* }}} */
2850
2851    /**
2852     * Returns a list of folders and error message not linked in the tree
2853     *
2854     * This method checks all folders in the database.
2855     *
2856     * @return array|bool
2857     */
2858    function checkFolders() { /* {{{ */
2859        $queryStr = "SELECT * FROM `tblFolders`";
2860        $resArr = $this->db->getResultArray($queryStr);
2861
2862        if (is_bool($resArr) && $resArr === false)
2863            return false;
2864
2865        $cache = array();
2866        foreach($resArr as $rec) {
2867            $cache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2868        }
2869        $errors = array();
2870        foreach($cache as $id=>$rec) {
2871            if(!array_key_exists($rec['parent'], $cache) && $rec['parent'] != 0) {
2872                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2873            }
2874            if(!isset($errors[$id]))    {
2875                /* Create the real folderList and compare it with the stored folderList */
2876                $parent = $rec['parent'];
2877                $fl = [];
2878                while($parent) {
2879                    array_unshift($fl, $parent);
2880                    $parent = $cache[$parent]['parent'];
2881                }
2882                if($fl)
2883                    $flstr = ':'.implode(':', $fl).':';
2884                else
2885                    $flstr = '';
2886                if($flstr != $rec['folderList'])
2887                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2888            }
2889            if(!isset($errors[$id]))    {
2890                /* This is the old insufficient test which will most likely not be called
2891                 * anymore, because the check for a wrong folder list will cache a folder
2892                 * list problem anyway.
2893                 */
2894                $tmparr = explode(':', $rec['folderList']);
2895                array_shift($tmparr);
2896                if(count($tmparr) != count(array_unique($tmparr))) {
2897                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].')');
2898                }
2899            }
2900        }
2901
2902        return $errors;
2903    } /* }}} */
2904
2905    /**
2906     * Returns a list of documents and error message not linked in the tree
2907     *
2908     * This method checks all documents in the database.
2909     *
2910     * @return array|bool
2911     */
2912    function checkDocuments() { /* {{{ */
2913        $queryStr = "SELECT * FROM `tblFolders`";
2914        $resArr = $this->db->getResultArray($queryStr);
2915
2916        if (is_bool($resArr) && $resArr === false)
2917            return false;
2918
2919        $fcache = array();
2920        foreach($resArr as $rec) {
2921            $fcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['parent'], 'folderList'=>$rec['folderList']);
2922        }
2923
2924        $queryStr = "SELECT * FROM `tblDocuments`";
2925        $resArr = $this->db->getResultArray($queryStr);
2926
2927        if (is_bool($resArr) && $resArr === false)
2928            return false;
2929
2930        $dcache = array();
2931        foreach($resArr as $rec) {
2932            $dcache[$rec['id']] = array('name'=>$rec['name'], 'parent'=>$rec['folder'], 'folderList'=>$rec['folderList']);
2933        }
2934        $errors = array();
2935        foreach($dcache as $id=>$rec) {
2936            if(!array_key_exists($rec['parent'], $fcache) && $rec['parent'] != 0) {
2937                $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Missing parent');
2938            }
2939            if(!isset($errors[$id]))    {
2940                /* Create the real folderList and compare it with the stored folderList */
2941                $parent = $rec['parent'];
2942                $fl = [];
2943                while($parent) {
2944                    array_unshift($fl, $parent);
2945                    $parent = $fcache[$parent]['parent'];
2946                }
2947                if($fl)
2948                    $flstr = ':'.implode(':', $fl).':';
2949                if($flstr != $rec['folderList'])
2950                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Wrong folder list '.$flstr.'!='.$rec['folderList']);
2951            }
2952            if(!isset($errors[$id]))    {
2953                $tmparr = explode(':', $rec['folderList']);
2954                array_shift($tmparr);
2955                if(count($tmparr) != count(array_unique($tmparr))) {
2956                    $errors[$id] = array('id'=>$id, 'name'=>$rec['name'], 'parent'=>$rec['parent'], 'msg'=>'Duplicate entry in folder list ('.$rec['folderList'].'');
2957                }
2958            }
2959        }
2960
2961        return $errors;
2962    } /* }}} */
2963
2964    /**
2965     * Return a user by its id
2966     *
2967     * This method retrieves a user from the database by its id.
2968     *
2969     * @param integer $id internal id of user
2970     * @return SeedDMS_Core_User|boolean instance of {@see SeedDMS_Core_User} or false
2971     */
2972    function getUser($id) { /* {{{ */
2973        if($this->usecache && isset($this->cache['users'][$id])) {
2974            return $this->cache['users'][$id];
2975        }
2976        $classname = $this->classnames['user'];
2977        $user = $classname::getInstance($id, $this);
2978        if($this->usecache)
2979            $this->cache['users'][$id] = $user;
2980        return $user;
2981    } /* }}} */
2982
2983    /**
2984     * Return a user by its login
2985     *
2986     * This method retrieves a user from the database by its login.
2987     * If the second optional parameter $email is not empty, the user must
2988     * also have the given email.
2989     *
2990     * @param string $login internal login of user
2991     * @param string $email email of user
2992     * @return object instance of {@see SeedDMS_Core_User} or false
2993     */
2994    function getUserByLogin($login, $email='') { /* {{{ */
2995        $classname = $this->classnames['user'];
2996        return $classname::getInstance($login, $this, 'name', $email);
2997    } /* }}} */
2998
2999    /**
3000     * Return a user by its email
3001     *
3002     * This method retrieves a user from the database by its email.
3003     * It is needed when the user requests a new password.
3004     *
3005     * @param integer $email email address of user
3006     * @return object instance of {@see SeedDMS_Core_User} or false in case of an error
3007     */
3008    function getUserByEmail($email) { /* {{{ */
3009        $classname = $this->classnames['user'];
3010        return $classname::getInstance($email, $this, 'email');
3011    } /* }}} */
3012
3013    /**
3014     * Return list of all users
3015     *
3016     * @param string $orderby
3017     * @return array list of instances of {@see SeedDMS_Core_User} or false in case of an error
3018     */
3019    function getAllUsers($orderby = '') { /* {{{ */
3020        $classname = $this->classnames['user'];
3021        return $classname::getAllInstances($orderby, $this);
3022    } /* }}} */
3023
3024    /**
3025     * Add a new user
3026     *
3027     * This method calls the hook `onPostAddUser` after the user has been
3028     * added successfully.
3029     *
3030     * @param string $login login name
3031     * @param string $pwd hashed password of new user
3032     * @param string $fullName full name of user
3033     * @param string $email Email of new user
3034     * @param string $language language of new user
3035     * @param string $theme theme
3036     * @param string $comment comment of new user
3037     * @param int|string $role role of new user (can be 0=normal, 1=admin, 2=guest)
3038     * @param integer $isHidden hide user in all lists, if this is set login
3039     *        is still allowed
3040     * @param integer $isDisabled disable user and prevent login
3041     * @param string $pwdexpiration
3042     * @param int $quota
3043     * @param null $homefolder
3044     * @return bool|SeedDMS_Core_User or false if the user already exists or in case of an error
3045     */
3046    function addUser($login, $pwd, $fullName, $email, $language, $theme, $comment, $role='3', $isHidden=0, $isDisabled=0, $pwdexpiration='', $quota=0, $homefolder=null) { /* {{{ */
3047        $db = $this->db;
3048        if (is_object($this->getUserByLogin($login))) {
3049            return false;
3050        }
3051        if(!is_object($role)) {
3052            if($role == '')
3053                $role = SeedDMS_Core_Role::getInstance(3, $this);
3054            else
3055                $role = SeedDMS_Core_Role::getInstance($role, $this);
3056        }
3057        if(trim($pwdexpiration) == '' || trim($pwdexpiration) == 'never') {
3058            $pwdexpiration = 'NULL';
3059        } elseif(trim($pwdexpiration) == 'now') {
3060            $pwdexpiration = $db->qstr(date('Y-m-d H:i:s'));
3061        } else {
3062            $pwdexpiration = $db->qstr($pwdexpiration);
3063        }
3064        $queryStr = "INSERT INTO `tblUsers` (`login`, `pwd`, `fullName`, `email`, `language`, `theme`, `comment`, `role`, `hidden`, `disabled`, `pwdExpiration`, `quota`, `homefolder`) VALUES (".$db->qstr($login).", ".$db->qstr($pwd).", ".$db->qstr($fullName).", ".$db->qstr($email).", '".$language."', '".$theme."', ".$db->qstr($comment).", '".intval($role->getId())."', '".intval($isHidden)."', '".intval($isDisabled)."', ".$pwdexpiration.", '".intval($quota)."', ".($homefolder ? intval($homefolder) : "NULL").")";
3065        $res = $this->db->getResult($queryStr);
3066        if (!$res)
3067            return false;
3068
3069        $user = $this->getUser($this->db->getInsertID('tblUsers'));
3070
3071        /* Check if 'onPostAddUser' callback is set */
3072        if(isset($this->callbacks['onPostAddUser'])) {
3073            foreach($this->callbacks['onPostAddUser'] as $callback) {
3074                /** @noinspection PhpStatementHasEmptyBodyInspection */
3075                if(!call_user_func($callback[0], $callback[1], $user)) {
3076                }
3077            }
3078        }
3079
3080        return $user;
3081    } /* }}} */
3082
3083    /**
3084     * Get a group by its id
3085     *
3086     * @param integer $id id of group
3087     * @return SeedDMS_Core_Group|boolean group or false if no group was found
3088     */
3089    function getGroup($id) { /* {{{ */
3090        if($this->usecache && isset($this->cache['groups'][$id])) {
3091            return $this->cache['groups'][$id];
3092        }
3093        $classname = $this->classnames['group'];
3094        $group = $classname::getInstance($id, $this, '');
3095        if($this->usecache)
3096            $this->cache['groups'][$id] = $group;
3097        return $group;
3098    } /* }}} */
3099
3100    /**
3101     * Get a group by its name
3102     *
3103     * @param string $name name of group
3104     * @return SeedDMS_Core_Group|boolean group or false if no group was found
3105     */
3106    function getGroupByName($name) { /* {{{ */
3107        $name = trim($name);
3108        $classname = $this->classnames['group'];
3109        return $classname::getInstance($name, $this, 'name');
3110    } /* }}} */
3111
3112    /**
3113     * Get a list of all groups
3114     *
3115     * @return SeedDMS_Core_Group[] array of instances of {@see SeedDMS_Core_Group}
3116     */
3117    function getAllGroups() { /* {{{ */
3118        $classname = $this->classnames['group'];
3119        return $classname::getAllInstances('name', $this);
3120    } /* }}} */
3121
3122    /**
3123     * Create a new user group
3124     *
3125     * @param string $name name of group
3126     * @param string $comment comment of group
3127     * @return SeedDMS_Core_Group|boolean instance of {@see SeedDMS_Core_Group} or false in
3128     *         case of an error.
3129     */
3130    function addGroup($name, $comment) { /* {{{ */
3131        $name = trim($name);
3132        if (is_object($this->getGroupByName($name))) {
3133            return false;
3134        }
3135
3136        $queryStr = "INSERT INTO `tblGroups` (`name`, `comment`) VALUES (".$this->db->qstr($name).", ".$this->db->qstr($comment).")";
3137        if (!$this->db->getResult($queryStr))
3138            return false;
3139
3140        $group = $this->getGroup($this->db->getInsertID('tblGroups'));
3141
3142        /* Check if 'onPostAddGroup' callback is set */
3143        if(isset($this->callbacks['onPostAddGroup'])) {
3144            foreach($this->callbacks['onPostAddGroup'] as $callback) {
3145                /** @noinspection PhpStatementHasEmptyBodyInspection */
3146                if(!call_user_func($callback[0], $callback[1], $group)) {
3147                }
3148            }
3149        }
3150
3151        return $group;
3152    } /* }}} */
3153
3154    /**
3155     * Get a role by its id
3156     *
3157     * @param integer $id id of role
3158     * @return object/boolean role or false if no role was found
3159     */
3160    function getRole($id) { /* {{{ */
3161        $classname = $this->classnames['role'];
3162        return $classname::getInstance($id, $this);
3163    } /* }}} */
3164
3165    /**
3166     * Get a role by its name
3167     *
3168     * @param integer $name name of role
3169     * @return object/boolean role or false if no role was found
3170     */
3171    function getRoleByName($name) { /* {{{ */
3172        $classname = $this->classnames['role'];
3173        return $classname::getInstance($name, $this, 'name');
3174    } /* }}} */
3175
3176    /**
3177     * Return list of all roles
3178     *
3179     * @return array of instances of {@link SeedDMS_Core_Role} or false
3180     */
3181    function getAllRoles($orderby = '') { /* {{{ */
3182        $classname = $this->classnames['role'];
3183        return $classname::getAllInstances($orderby, $this);
3184    } /* }}} */
3185
3186    /**
3187     * Create a new role
3188     *
3189     * @param string $name name of role
3190     * @return object/boolean instance of {@link SeedDMS_Core_Role} or false in
3191     *         case of an error.
3192     */
3193    function addRole($name, $role) { /* {{{ */
3194        if (is_object($this->getRoleByName($name))) {
3195            return false;
3196        }
3197
3198        $queryStr = "INSERT INTO `tblRoles` (`name`, `role`) VALUES (".$this->db->qstr($name).", ".$role.")";
3199        if (!$this->db->getResult($queryStr))
3200            return false;
3201
3202        return $this->getRole($this->db->getInsertID('tblRoles'));
3203    } /* }}} */
3204
3205    /**
3206     * Get a transmittal by its id
3207     *
3208     * @param integer $id id of transmittal
3209     * @return object/boolean transmittal or false if no group was found
3210     */
3211    function getTransmittal($id) { /* {{{ */
3212        $classname = $this->classnames['transmittal'];
3213        return $classname::getInstance($id, $this, '');
3214    } /* }}} */
3215
3216    /**
3217     * Get a transmittal by its name
3218     *
3219     * @param string $name name of transmittal
3220     * @return object/boolean transmittal or false if no group was found
3221     */
3222    function getTransmittalByName($name) { /* {{{ */
3223        $classname = $this->classnames['transmittal'];
3224        return $classname::getInstance($name, $this, 'name');
3225    } /* }}} */
3226
3227    /**
3228     * Return list of all transmittals
3229     *
3230     * @return array of instances of {@link SeedDMS_Core_Transmittal} or false
3231     */
3232    function getAllTransmittals($user=null, $orderby = '') { /* {{{ */
3233        $classname = $this->classnames['transmittal'];
3234        return $classname::getAllInstances($user, $orderby, $this);
3235    } /* }}} */
3236
3237    /**
3238     * Create a new transmittal
3239     *
3240     * @param string $name name of group
3241     * @param string $comment comment of group
3242     * @param object $user user this transmittal belongs to
3243     * @return object/boolean instance of {@link SeedDMS_Core_Transmittal} or
3244     *         false in case of an error.
3245     */
3246    function addTransmittal($name, $comment, $user) { /* {{{ */
3247        if (is_object($this->getTransmittalByName($name))) {
3248            return false;
3249        }
3250
3251        $queryStr = "INSERT INTO `tblTransmittals` (`name`, `comment`, `userID`) VALUES (".$this->db->qstr($name).", ".$this->db->qstr($comment).", ".$user->getID().")";
3252        if (!$this->db->getResult($queryStr))
3253            return false;
3254
3255        return $this->getTransmittal($this->db->getInsertID('tblTransmittals'));
3256    } /* }}} */
3257
3258    function getKeywordCategory($id) { /* {{{ */
3259        if (!is_numeric($id) || $id < 1)
3260            return false;
3261
3262        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `id` = " . (int) $id;
3263        $resArr = $this->db->getResultArray($queryStr);
3264        if (is_bool($resArr) && !$resArr)
3265            return false;
3266        if (count($resArr) != 1)
3267            return null;
3268
3269        $resArr = $resArr[0];
3270        $cat = new SeedDMS_Core_Keywordcategory($resArr["id"], $resArr["owner"], $resArr["name"]);
3271        $cat->setDMS($this);
3272        return $cat;
3273    } /* }}} */
3274
3275    function getKeywordCategoryByName($name, $userID) { /* {{{ */
3276        if (!is_numeric($userID) || $userID < 1)
3277            return false;
3278        $name = trim($name);
3279        $queryStr = "SELECT * FROM `tblKeywordCategories` WHERE `name` = " . $this->db->qstr($name) . " AND `owner` = " . (int) $userID;
3280        $resArr = $this->db->getResultArray($queryStr);
3281        if (is_bool($resArr) && !$resArr)
3282            return false;
3283        if (count($resArr) != 1)
3284            return null;
3285
3286        $resArr = $resArr[0];
3287        $cat = new SeedDMS_Core_Keywordcategory($resArr["id"], $resArr["owner"], $resArr["name"]);
3288        $cat->setDMS($this);
3289        return $cat;
3290    } /* }}} */
3291
3292    function getAllKeywordCategories($userIDs = array()) { /* {{{ */
3293        $queryStr = "SELECT * FROM `tblKeywordCategories`";
3294        /* Ensure $userIDs() will only contain integers > 0 */
3295        $userIDs = array_filter(array_unique(array_map('intval', $userIDs)), function($a) {return $a > 0;});
3296        if ($userIDs) {
3297            $queryStr .= " WHERE `owner` IN (".implode(',', $userIDs).")";
3298        }
3299
3300        $resArr = $this->db->getResultArray($queryStr);
3301        if (is_bool($resArr) && !$resArr)
3302            return false;
3303
3304        $categories = array();
3305        foreach ($resArr as $row) {
3306            $cat = new SeedDMS_Core_KeywordCategory($row["id"], $row["owner"], $row["name"]);
3307            $cat->setDMS($this);
3308            array_push($categories, $cat);
3309        }
3310
3311        return $categories;
3312    } /* }}} */
3313
3314    /**
3315     * This method should be replaced by getAllKeywordCategories()
3316     *
3317     * @param $userID
3318     * @return SeedDMS_Core_KeywordCategory[]|bool
3319     */
3320    function getAllUserKeywordCategories($userID) { /* {{{ */
3321        if (!is_numeric($userID) || $userID < 1)
3322            return false;
3323        return self::getAllKeywordCategories([$userID]);
3324    } /* }}} */
3325
3326    function addKeywordCategory($userID, $name) { /* {{{ */
3327        if (!is_numeric($userID) || $userID < 1)
3328            return false;
3329        $name = trim($name);
3330        if(!$name)
3331            return false;
3332        if (is_object($this->getKeywordCategoryByName($name, $userID))) {
3333            return false;
3334        }
3335        $queryStr = "INSERT INTO `tblKeywordCategories` (`owner`, `name`) VALUES (".(int) $userID.", ".$this->db->qstr($name).")";
3336        if (!$this->db->getResult($queryStr))
3337            return false;
3338
3339        $category = $this->getKeywordCategory($this->db->getInsertID('tblKeywordCategories'));
3340
3341        /* Check if 'onPostAddKeywordCategory' callback is set */
3342        if(isset($this->callbacks['onPostAddKeywordCategory'])) {
3343            foreach($this->callbacks['onPostAddKeywordCategory'] as $callback) {
3344                /** @noinspection PhpStatementHasEmptyBodyInspection */
3345                if(!call_user_func($callback[0], $callback[1], $category)) {
3346                }
3347            }
3348        }
3349
3350        return $category;
3351    } /* }}} */
3352
3353    function getDocumentCategory($id) { /* {{{ */
3354        if (!is_numeric($id) || $id < 1)
3355            return false;
3356
3357        $queryStr = "SELECT * FROM `tblCategory` WHERE `id` = " . (int) $id;
3358        $resArr = $this->db->getResultArray($queryStr);
3359        if (is_bool($resArr) && !$resArr)
3360            return false;
3361        if (count($resArr) != 1)
3362            return null;
3363
3364        $resArr = $resArr[0];
3365        $cat = new SeedDMS_Core_DocumentCategory($resArr["id"], $resArr["name"]);
3366        $cat->setDMS($this);
3367        return $cat;
3368    } /* }}} */
3369
3370    function getDocumentCategories() { /* {{{ */
3371        $queryStr = "SELECT * FROM `tblCategory` order by `name`";
3372
3373        $resArr = $this->db->getResultArray($queryStr);
3374        if (is_bool($resArr) && !$resArr)
3375            return false;
3376
3377        $categories = array();
3378        foreach ($resArr as $row) {
3379            $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
3380            $cat->setDMS($this);
3381            array_push($categories, $cat);
3382        }
3383
3384        return $categories;
3385    } /* }}} */
3386
3387    /**
3388     * Get a category by its name
3389     *
3390     * The name of a category is by default unique.
3391     *
3392     * @param string $name human readable name of category
3393     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory}
3394     */
3395    function getDocumentCategoryByName($name) { /* {{{ */
3396        $name = trim($name);
3397        if (!$name) return false;
3398
3399        $queryStr = "SELECT * FROM `tblCategory` WHERE `name`=".$this->db->qstr($name);
3400        $resArr = $this->db->getResultArray($queryStr);
3401        if (!$resArr)
3402            return false;
3403
3404        $row = $resArr[0];
3405        $cat = new SeedDMS_Core_DocumentCategory($row["id"], $row["name"]);
3406        $cat->setDMS($this);
3407
3408        return $cat;
3409    } /* }}} */
3410
3411    /**
3412     * Add a new document category
3413     *
3414     * This method calls the hook `onPostAddDocumentCategory` if the new
3415     * category was added successfully.
3416     *
3417     * @param string $name name of category
3418     * @return SeedDMS_Core_DocumentCategory|boolean instance of {@see SeedDMS_Core_DocumentCategory} or false if the category already exists or in case of an error.
3419     */
3420    function addDocumentCategory($name) { /* {{{ */
3421        $name = trim($name);
3422        if(!$name)
3423            return false;
3424        if (is_object($this->getDocumentCategoryByName($name))) {
3425            return false;
3426        }
3427        $queryStr = "INSERT INTO `tblCategory` (`name`) VALUES (".$this->db->qstr($name).")";
3428        if (!$this->db->getResult($queryStr))
3429            return false;
3430
3431        $category = $this->getDocumentCategory($this->db->getInsertID('tblCategory'));
3432
3433        /* Check if 'onPostAddDocumentCategory' callback is set */
3434        if(isset($this->callbacks['onPostAddDocumentCategory'])) {
3435            foreach($this->callbacks['onPostAddDocumentCategory'] as $callback) {
3436                /** @noinspection PhpStatementHasEmptyBodyInspection */
3437                if(!call_user_func($callback[0], $callback[1], $category)) {
3438                }
3439            }
3440        }
3441
3442        return $category;
3443    } /* }}} */
3444
3445    /**
3446     * Get all notifications for a group
3447     *
3448     * deprecated: User {@see SeedDMS_Core_Group::getNotifications()}
3449     *
3450     * @param object $group group for which notifications are to be retrieved
3451     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
3452     * @return array array of notifications
3453     */
3454    function getNotificationsByGroup($group, $type=0) { /* {{{ */
3455        return $group->getNotifications($type);
3456    } /* }}} */
3457
3458    /**
3459     * Get all notifications for a user
3460     *
3461     * deprecated: User {@see SeedDMS_Core_User::getNotifications()}
3462     *
3463     * @param object $user user for which notifications are to be retrieved
3464     * @param integer $type type of item (T_DOCUMENT or T_FOLDER)
3465     * @return array array of notifications
3466     */
3467    function getNotificationsByUser($user, $type=0) { /* {{{ */
3468        return $user->getNotifications($type);
3469    } /* }}} */
3470
3471    /**
3472     * Create a token to request a new password.
3473     *
3474     * This method will not delete the password but just creates an entry
3475     * in `tblUserRequestPassword` indicating a password request.
3476     *
3477     * @param SeedDMS_Core_User $user
3478     * @return string|boolean hash value of false in case of an error
3479     */
3480    function createPasswordRequest($user) { /* {{{ */
3481        $lenght = 32;
3482        if (function_exists("random_bytes")) {
3483            $bytes = random_bytes((int) ceil($lenght / 2));
3484        } elseif (function_exists("openssl_random_pseudo_bytes")) {
3485            $bytes = openssl_random_pseudo_bytes(ceil($lenght / 2));
3486        } else {
3487            return false;
3488        }
3489        $hash = bin2hex($bytes);
3490        $queryStr = "INSERT INTO `tblUserPasswordRequest` (`userID`, `hash`, `date`) VALUES (" . $user->getId() . ", " . $this->db->qstr($hash) .", ".$this->db->getCurrentDatetime().")";
3491        $resArr = $this->db->getResult($queryStr);
3492        if (is_bool($resArr) && !$resArr) return false;
3493        return $hash;
3494
3495    } /* }}} */
3496
3497    /**
3498     * Check if hash for a password request is valid.
3499     *
3500     * This method searches a previously created password request and
3501     * returns the user.
3502     *
3503     * @param string $hash
3504     * @return bool|SeedDMS_Core_User
3505     */
3506    function checkPasswordRequest($hash) { /* {{{ */
3507        /* Get the password request from the database */
3508        $queryStr = "SELECT * FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
3509        $resArr = $this->db->getResultArray($queryStr);
3510        if (is_bool($resArr) && !$resArr)
3511            return false;
3512
3513        if (count($resArr) != 1)
3514            return false;
3515        $resArr = $resArr[0];
3516
3517        return $this->getUser($resArr['userID']);
3518
3519    } /* }}} */
3520
3521    /**
3522     * Delete a password request
3523     *
3524     * @param string $hash
3525     * @return bool
3526     */
3527    function deletePasswordRequest($hash) { /* {{{ */
3528        /* Delete the request, so nobody can use it a second time */
3529        $queryStr = "DELETE FROM `tblUserPasswordRequest` WHERE `hash`=".$this->db->qstr($hash);
3530        if (!$this->db->getResult($queryStr))
3531            return false;
3532        return true;
3533    } /* }}} */
3534
3535    /**
3536     * Return a attribute definition by its id
3537     *
3538     * This method retrieves a attribute definitionr from the database by
3539     * its id.
3540     *
3541     * @param integer $id internal id of attribute defintion
3542     * @return bool|SeedDMS_Core_AttributeDefinition or false
3543     */
3544    function getAttributeDefinition($id) { /* {{{ */
3545        if (!is_numeric($id) || $id < 1)
3546            return false;
3547
3548        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `id` = " . (int) $id;
3549        $resArr = $this->db->getResultArray($queryStr);
3550
3551        if (is_bool($resArr) && $resArr == false)
3552            return false;
3553        if (count($resArr) != 1)
3554            return null;
3555
3556        $resArr = $resArr[0];
3557
3558        $attrdef = new SeedDMS_Core_AttributeDefinition($resArr["id"], $resArr["name"], (int) $resArr["objtype"], (int) $resArr["type"], $resArr["multiple"], $resArr["minvalues"], $resArr["maxvalues"], $resArr["valueset"], $resArr["regex"]);
3559        $attrdef->setDMS($this);
3560        return $attrdef;
3561    } /* }}} */
3562
3563    /**
3564     * Return a attribute definition by its name
3565     *
3566     * This method retrieves an attribute def. from the database by its name.
3567     *
3568     * @param string $name internal name of attribute def.
3569     * @return SeedDMS_Core_AttributeDefinition|boolean instance of {@see SeedDMS_Core_AttributeDefinition} or false
3570     */
3571    function getAttributeDefinitionByName($name) { /* {{{ */
3572        $name = trim($name);
3573        if (!$name) return false;
3574
3575        $queryStr = "SELECT * FROM `tblAttributeDefinitions` WHERE `name` = " . $this->db->qstr($name);
3576        $resArr = $this->db->getResultArray($queryStr);
3577
3578        if (is_bool($resArr) && $resArr == false)
3579            return false;
3580        if (count($resArr) != 1)
3581            return null;
3582
3583        $resArr = $resArr[0];
3584
3585        $attrdef = new SeedDMS_Core_AttributeDefinition($resArr["id"], $resArr["name"], (int) $resArr["objtype"], (int) $resArr["type"], $resArr["multiple"], $resArr["minvalues"], $resArr["maxvalues"], $resArr["valueset"], $resArr["regex"]);
3586        $attrdef->setDMS($this);
3587        return $attrdef;
3588    } /* }}} */
3589
3590    /**
3591     * Return list of all attribute definitions
3592     *
3593     * @param integer|array $objtype select those attribute definitions defined for an object type
3594     * @param integer|array $type select those attribute definitions defined for a type
3595     * @return bool|SeedDMS_Core_AttributeDefinition[] of instances of {@see SeedDMS_Core_AttributeDefinition} or false
3596     * or false
3597     */
3598    function getAllAttributeDefinitions($objtype=0, $type=0) { /* {{{ */
3599        $queryStr = "SELECT * FROM `tblAttributeDefinitions`";
3600        if($objtype || $type) {
3601            $queryStr .= ' WHERE ';
3602            if($objtype) {
3603                if(is_array($objtype))
3604                    $queryStr .= '`objtype` in (\''.implode("','", $objtype).'\')';
3605                else
3606                    $queryStr .= '`objtype`='.intval($objtype);
3607            }
3608            if($objtype && $type) {
3609                $queryStr .= ' AND ';
3610            }
3611            if($type) {
3612                if(is_array($type))
3613                    $queryStr .= '`type` in (\''.implode("','", $type).'\')';
3614                else
3615                    $queryStr .= '`type`='.intval($type);
3616            }
3617        }
3618        $queryStr .= ' ORDER BY `name`';
3619        $resArr = $this->db->getResultArray($queryStr);
3620
3621        if (is_bool($resArr) && $resArr == false)
3622            return false;
3623
3624        /** @var SeedDMS_Core_AttributeDefinition[] $attrdefs */
3625        $attrdefs = array();
3626
3627        for ($i = 0; $i < count($resArr); $i++) {
3628            $attrdef = new SeedDMS_Core_AttributeDefinition($resArr[$i]["id"], $resArr[$i]["name"], (int) $resArr[$i]["objtype"], (int) $resArr[$i]["type"], $resArr[$i]["multiple"], $resArr[$i]["minvalues"], $resArr[$i]["maxvalues"], $resArr[$i]["valueset"], $resArr[$i]["regex"]);
3629            $attrdef->setDMS($this);
3630            $attrdefs[$i] = $attrdef;
3631        }
3632
3633        return $attrdefs;
3634    } /* }}} */
3635
3636    /**
3637     * Add a new attribute definition
3638     *
3639     * @param string $name name of attribute
3640     * @param $objtype
3641     * @param string $type type of attribute
3642     * @param bool|int $multiple set to 1 if attribute has multiple attributes
3643     * @param integer $minvalues minimum number of values
3644     * @param integer $maxvalues maximum number of values if multiple is set
3645     * @param string $valueset list of allowed values (csv format)
3646     * @param string $regex
3647     * @return bool|SeedDMS_Core_User
3648     */
3649    function addAttributeDefinition($name, $objtype, $type, $multiple=0, $minvalues=0, $maxvalues=1, $valueset='', $regex='') { /* {{{ */
3650        $name = trim($name);
3651        if(!$name)
3652            return false;
3653        if (is_object($this->getAttributeDefinitionByName($name))) {
3654            return false;
3655        }
3656        if($objtype < SeedDMS_Core_AttributeDefinition::objtype_all || $objtype > SeedDMS_Core_AttributeDefinition::objtype_documentcontent)
3657            return false;
3658        if(!$type)
3659            return false;
3660        if(trim($valueset)) {
3661            $valuesetarr = array_map('trim', explode($valueset[0], substr($valueset, 1)));
3662            $valueset = $valueset[0].implode($valueset[0], $valuesetarr);
3663        } else {
3664            $valueset = '';
3665        }
3666        $queryStr = "INSERT INTO `tblAttributeDefinitions` (`name`, `objtype`, `type`, `multiple`, `minvalues`, `maxvalues`, `valueset`, `regex`) VALUES (".$this->db->qstr($name).", ".intval($objtype).", ".intval($type).", ".intval($multiple).", ".intval($minvalues).", ".intval($maxvalues).", ".$this->db->qstr($valueset).", ".$this->db->qstr($regex).")";
3667        $res = $this->db->getResult($queryStr);
3668        if (!$res)
3669            return false;
3670
3671        return $this->getAttributeDefinition($this->db->getInsertID('tblAttributeDefinitions'));
3672    } /* }}} */
3673
3674    /**
3675     * Return list of all workflows
3676     *
3677     * @return SeedDMS_Core_Workflow[]|bool of instances of {@see SeedDMS_Core_Workflow} or false
3678     */
3679    function getAllWorkflows() { /* {{{ */
3680        $queryStr = "SELECT * FROM `tblWorkflows` ORDER BY `name`";
3681        $resArr = $this->db->getResultArray($queryStr);
3682
3683        if (is_bool($resArr) && $resArr == false)
3684            return false;
3685
3686        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
3687        $ressArr = $this->db->getResultArray($queryStr);
3688
3689        if (is_bool($ressArr) && $ressArr == false)
3690            return false;
3691
3692        for ($i = 0; $i < count($ressArr); $i++) {
3693            $wkfstates[$ressArr[$i]["id"]] = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
3694        }
3695
3696        /** @var SeedDMS_Core_Workflow[] $workflows */
3697        $workflows = array();
3698        for ($i = 0; $i < count($resArr); $i++) {
3699            /** @noinspection PhpUndefinedVariableInspection */
3700            $workflow = new SeedDMS_Core_Workflow($resArr[$i]["id"], $resArr[$i]["name"], $wkfstates[$resArr[$i]["initstate"]], $resArr[$i]["layoutdata"]);
3701            $workflow->setDMS($this);
3702            $workflows[$i] = $workflow;
3703        }
3704
3705        return $workflows;
3706    } /* }}} */
3707
3708    /**
3709     * Return workflow by its Id
3710     *
3711     * @param integer $id internal id of workflow
3712     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow}, null if no workflow was found or false
3713     */
3714    function getWorkflow($id) { /* {{{ */
3715        if (!is_numeric($id) || $id < 1)
3716            return false;
3717
3718        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `id`=".intval($id);
3719        $resArr = $this->db->getResultArray($queryStr);
3720
3721        if (is_bool($resArr) && $resArr == false)
3722            return false;
3723
3724        if(!$resArr)
3725            return null;
3726
3727        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
3728
3729        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate, $resArr[0]["layoutdata"]);
3730        $workflow->setDMS($this);
3731
3732        return $workflow;
3733    } /* }}} */
3734
3735    /**
3736     * Return workflow by its name
3737     *
3738     * @param string $name name of workflow
3739     * @return SeedDMS_Core_Workflow|bool of instances of {@see SeedDMS_Core_Workflow} or null if no workflow was found or false
3740     */
3741    function getWorkflowByName($name) { /* {{{ */
3742        $name = trim($name);
3743        if (!$name) return false;
3744
3745        $queryStr = "SELECT * FROM `tblWorkflows` WHERE `name`=".$this->db->qstr($name);
3746        $resArr = $this->db->getResultArray($queryStr);
3747
3748        if (is_bool($resArr) && $resArr == false)
3749            return false;
3750
3751        if(!$resArr)
3752            return null;
3753
3754        $initstate = $this->getWorkflowState($resArr[0]['initstate']);
3755
3756        $workflow = new SeedDMS_Core_Workflow($resArr[0]["id"], $resArr[0]["name"], $initstate, $resArr[0]["layoutdata"]);
3757        $workflow->setDMS($this);
3758
3759        return $workflow;
3760    } /* }}} */
3761
3762    /**
3763     * Add a new workflow
3764     *
3765     * @param string $name name of workflow
3766     * @param SeedDMS_Core_Workflow_State $initstate initial state of workflow
3767     * @return bool|SeedDMS_Core_Workflow
3768     */
3769    function addWorkflow($name, $initstate) { /* {{{ */
3770        $db = $this->db;
3771        $name = trim($name);
3772        if(!$name)
3773            return false;
3774        if (is_object($this->getWorkflowByName($name))) {
3775            return false;
3776        }
3777        $queryStr = "INSERT INTO `tblWorkflows` (`name`, `initstate`) VALUES (".$db->qstr($name).", ".$initstate->getID().")";
3778        $res = $db->getResult($queryStr);
3779        if (!$res)
3780            return false;
3781
3782        return $this->getWorkflow($db->getInsertID('tblWorkflows'));
3783    } /* }}} */
3784
3785    /**
3786     * Return a workflow state by its id
3787     *
3788     * This method retrieves a workflow state from the database by its id.
3789     *
3790     * @param integer $id internal id of workflow state
3791     * @return bool|SeedDMS_Core_Workflow_State or false
3792     */
3793    function getWorkflowState($id) { /* {{{ */
3794        if (!is_numeric($id) || $id < 1)
3795            return false;
3796
3797        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `id` = " . (int) $id;
3798        $resArr = $this->db->getResultArray($queryStr);
3799
3800        if (is_bool($resArr) && $resArr == false)
3801            return false;
3802
3803        if (count($resArr) != 1)
3804             return null;
3805
3806        $resArr = $resArr[0];
3807
3808        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3809        $state->setDMS($this);
3810        return $state;
3811    } /* }}} */
3812
3813    /**
3814     * Return workflow state by its name
3815     *
3816     * @param string $name name of workflow state
3817     * @return bool|SeedDMS_Core_Workflow_State or false
3818     */
3819    function getWorkflowStateByName($name) { /* {{{ */
3820        $name = trim($name);
3821        if (!$name) return false;
3822
3823        $queryStr = "SELECT * FROM `tblWorkflowStates` WHERE `name`=".$this->db->qstr($name);
3824        $resArr = $this->db->getResultArray($queryStr);
3825
3826        if (is_bool($resArr) && $resArr == false)
3827            return false;
3828
3829        if(!$resArr)
3830            return null;
3831
3832        $resArr = $resArr[0];
3833
3834        $state = new SeedDMS_Core_Workflow_State($resArr["id"], $resArr["name"], $resArr["maxtime"], $resArr["precondfunc"], $resArr["documentstatus"]);
3835        $state->setDMS($this);
3836
3837        return $state;
3838    } /* }}} */
3839
3840    /**
3841     * Return list of all workflow states
3842     *
3843     * @return SeedDMS_Core_Workflow_State[]|bool of instances of {@see SeedDMS_Core_Workflow_State} or false
3844     */
3845    function getAllWorkflowStates() { /* {{{ */
3846        $queryStr = "SELECT * FROM `tblWorkflowStates` ORDER BY `name`";
3847        $ressArr = $this->db->getResultArray($queryStr);
3848
3849        if (is_bool($ressArr) && $ressArr == false)
3850            return false;
3851
3852        $wkfstates = array();
3853        for ($i = 0; $i < count($ressArr); $i++) {
3854            $wkfstate = new SeedDMS_Core_Workflow_State($ressArr[$i]["id"], $ressArr[$i]["name"], $ressArr[$i]["maxtime"], $ressArr[$i]["precondfunc"], $ressArr[$i]["documentstatus"]);
3855            $wkfstate->setDMS($this);
3856            $wkfstates[$i] = $wkfstate;
3857        }
3858
3859        return $wkfstates;
3860    } /* }}} */
3861
3862    /**
3863     * Add new workflow state
3864     *
3865     * @param string $name name of workflow state
3866     * @param integer $docstatus document status when this state is reached
3867     * @return bool|SeedDMS_Core_Workflow_State
3868     */
3869    function addWorkflowState($name, $docstatus) { /* {{{ */
3870        $db = $this->db;
3871        $name = trim($name);
3872        if(!$name)
3873            return false;
3874        if (is_object($this->getWorkflowStateByName($name))) {
3875            return false;
3876        }
3877        $queryStr = "INSERT INTO `tblWorkflowStates` (`name`, `documentstatus`) VALUES (".$db->qstr($name).", ".(int) $docstatus.")";
3878        $res = $db->getResult($queryStr);
3879        if (!$res)
3880            return false;
3881
3882        return $this->getWorkflowState($db->getInsertID('tblWorkflowStates'));
3883    } /* }}} */
3884
3885    /**
3886     * Return a workflow action by its id
3887     *
3888     * This method retrieves a workflow action from the database by its id.
3889     *
3890     * @param integer $id internal id of workflow action
3891     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3892     */
3893    function getWorkflowAction($id) { /* {{{ */
3894        if (!is_numeric($id) || $id < 1)
3895            return false;
3896
3897        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `id` = " . (int) $id;
3898        $resArr = $this->db->getResultArray($queryStr);
3899
3900        if (is_bool($resArr) && $resArr == false)
3901            return false;
3902
3903        if (count($resArr) != 1)
3904             return null;
3905
3906        $resArr = $resArr[0];
3907
3908        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3909        $action->setDMS($this);
3910        return $action;
3911    } /* }}} */
3912
3913    /**
3914     * Return a workflow action by its name
3915     *
3916     * This method retrieves a workflow action from the database by its name.
3917     *
3918     * @param string $name name of workflow action
3919     * @return SeedDMS_Core_Workflow_Action|bool instance of {@see SeedDMS_Core_Workflow_Action} or false
3920     */
3921    function getWorkflowActionByName($name) { /* {{{ */
3922        $name = trim($name);
3923        if (!$name) return false;
3924
3925        $queryStr = "SELECT * FROM `tblWorkflowActions` WHERE `name` = " . $this->db->qstr($name);
3926        $resArr = $this->db->getResultArray($queryStr);
3927
3928        if (is_bool($resArr) && $resArr == false)
3929            return false;
3930
3931        if (count($resArr) != 1)
3932             return null;
3933
3934        $resArr = $resArr[0];
3935
3936        $action = new SeedDMS_Core_Workflow_Action($resArr["id"], $resArr["name"]);
3937        $action->setDMS($this);
3938        return $action;
3939    } /* }}} */
3940
3941    /**
3942     * Return list of workflow action
3943     *
3944     * @return SeedDMS_Core_Workflow_Action[]|bool list of instances of {@see SeedDMS_Core_Workflow_Action} or false
3945     */
3946    function getAllWorkflowActions() { /* {{{ */
3947        $queryStr = "SELECT * FROM `tblWorkflowActions`";
3948        $resArr = $this->db->getResultArray($queryStr);
3949
3950        if (is_bool($resArr) && $resArr == false)
3951            return false;
3952
3953        /** @var SeedDMS_Core_Workflow_Action[] $wkfactions */
3954        $wkfactions = array();
3955        for ($i = 0; $i < count($resArr); $i++) {
3956            $action = new SeedDMS_Core_Workflow_Action($resArr[$i]["id"], $resArr[$i]["name"]);
3957            $action->setDMS($this);
3958            $wkfactions[$i] = $action;
3959        }
3960
3961        return $wkfactions;
3962    } /* }}} */
3963
3964    /**
3965     * Add new workflow action
3966     *
3967     * @param string $name name of workflow action
3968     * @return SeedDMS_Core_Workflow_Action|bool
3969     */
3970    function addWorkflowAction($name) { /* {{{ */
3971        $db = $this->db;
3972        $name = trim($name);
3973        if(!$name)
3974            return false;
3975        if (is_object($this->getWorkflowActionByName($name))) {
3976            return false;
3977        }
3978        $queryStr = "INSERT INTO `tblWorkflowActions` (`name`) VALUES (".$db->qstr($name).")";
3979        $res = $db->getResult($queryStr);
3980        if (!$res)
3981            return false;
3982
3983        return $this->getWorkflowAction($db->getInsertID('tblWorkflowActions'));
3984    } /* }}} */
3985
3986    /**
3987     * Return a workflow transition by its id
3988     *
3989     * This method retrieves a workflow transition from the database by its id.
3990     *
3991     * @param integer $id internal id of workflow transition
3992     * @return SeedDMS_Core_Workflow_Transition|bool instance of {@see SeedDMS_Core_Workflow_Transition} or false
3993     */
3994    function getWorkflowTransition($id) { /* {{{ */
3995        if (!is_numeric($id))
3996            return false;
3997
3998        $queryStr = "SELECT * FROM `tblWorkflowTransitions` WHERE `id` = " . (int) $id;
3999        $resArr = $this->db->getResultArray($queryStr);
4000
4001        if (is_bool($resArr) && $resArr == false) return false;
4002        if (count($resArr) != 1) return false;
4003
4004        $resArr = $resArr[0];
4005
4006        $transition = new SeedDMS_Core_Workflow_Transition($resArr["id"], $this->getWorkflow($resArr["workflow"]), $this->getWorkflowState($resArr["state"]), $this->getWorkflowAction($resArr["action"]), $this->getWorkflowState($resArr["nextstate"]), $resArr["maxtime"]);
4007        $transition->setDMS($this);
4008        return $transition;
4009    } /* }}} */
4010
4011    /**
4012     * Return all waiting receptions
4013     *
4014     * This function retrieves all waiting receptions and it associated
4015     * document version
4016     *
4017     * {see SeedDMS_Core_DMS::getDocumentsInRevision()}
4018     *
4019     * @return array list of receptions or false in case of an error
4020     */
4021    function getDocumentsInReception() { /* {{{ */
4022        if (!$this->db->createTemporaryTable("ttreceiptid") || !$this->db->createTemporaryTable("ttcontentid")) {
4023            return false;
4024        }
4025        $queryStr =
4026            "SELECT `tblDocumentRecipients`.*, `tblDocumentReceiptLog`.`status` FROM `tblDocumentRecipients` LEFT JOIN `ttreceiptid` ON `tblDocumentRecipients`.`receiptID` = `ttreceiptid`.`receiptID` LEFT JOIN `tblDocumentReceiptLog` ON `ttreceiptid`.`maxLogID` = `tblDocumentReceiptLog`.`receiptLogID` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion`=`tblDocumentRecipients`.`version` AND `ttcontentid`.`document`=`tblDocumentRecipients`.`documentID` WHERE `tblDocumentReceiptLog`.`status`=0 AND `ttcontentid`.`maxVersion` IS NOT NULL";
4027        $resArr = $this->db->getResultArray($queryStr);
4028
4029        return $resArr;
4030    } /* }}} */
4031
4032    /**
4033     * Return all documents revisors waiting for a revision to start (sleeping)
4034     * or are required to revise the document (waiting)
4035     *
4036     * This function retrieves all revisors which are waiting for
4037     * revision or already in revision
4038     * Note: the name of the method is somewhat misleading, because it
4039     * does not return documents but just database records from table
4040     * tblDocumentRevisors and tblDocumentRevisionLog
4041     *
4042     * @return array list of revisors or false in case of an error
4043     */
4044    function getDocumentsInRevision() { /* {{{ */
4045        if (!$this->db->createTemporaryTable("ttrevisionid") || !$this->db->createTemporaryTable("ttcontentid")) {
4046            return false;
4047        }
4048        $queryStr =
4049            "SELECT `tblDocumentRevisors`.*, `tblDocumentRevisionLog`.`status` FROM `tblDocumentRevisors` LEFT JOIN `ttrevisionid` ON `tblDocumentRevisors`.`revisionID` = `ttrevisionid`.`revisionID` LEFT JOIN `tblDocumentRevisionLog` ON `ttrevisionid`.`maxLogID` = `tblDocumentRevisionLog`.`revisionLogID` LEFT JOIN `ttcontentid` ON `ttcontentid`.`maxVersion`=`tblDocumentRevisors`.`version` AND `ttcontentid`.`document`=`tblDocumentRevisors`.`documentID` WHERE `tblDocumentRevisionLog`.`status` in (".S_LOG_WAITING.", ".S_LOG_SLEEPING.") AND `ttcontentid`.`maxVersion` IS NOT NULL";
4050        $resArr = $this->db->getResultArray($queryStr);
4051
4052        return $resArr;
4053    } /* }}} */
4054
4055    /**
4056     * Returns document content which is not linked to a document
4057     *
4058     * This method is for finding straying document content without
4059     * a parent document. In normal operation this should not happen
4060     * but little checks for database consistency and possible errors
4061     * in the application may have left over document content though
4062     * the document is gone already.
4063     *
4064     * @return array|bool
4065     */
4066    function getUnlinkedDocumentContent() { /* {{{ */
4067        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `document` NOT IN (SELECT id FROM `tblDocuments`)";
4068        $resArr = $this->db->getResultArray($queryStr);
4069        if ($resArr === false)
4070            return false;
4071
4072        $versions = array();
4073        foreach($resArr as $row) {
4074            /** @var SeedDMS_Core_Document $document */
4075            $document = new $this->classnames['document']($row['document'], '', '', '', '', '', '', '', '', '', '', '');
4076            $document->setDMS($this);
4077            $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
4078            $versions[] = $version;
4079        }
4080        return $versions;
4081
4082    } /* }}} */
4083
4084    /**
4085     * Returns document content which has no file size set
4086     *
4087     * This method is for finding document content without a file size
4088     * set in the database. The file size of a document content was introduced
4089     * in version 4.0.0 of SeedDMS for implementation of user quotas.
4090     *
4091     * @return SeedDMS_Core_Document[]|bool
4092     */
4093    function getNoFileSizeDocumentContent() { /* {{{ */
4094        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `fileSize` = 0 OR `fileSize` is null";
4095        $resArr = $this->db->getResultArray($queryStr);
4096        if ($resArr === false)
4097            return false;
4098
4099        /** @var SeedDMS_Core_Document[] $versions */
4100        $versions = array();
4101        foreach($resArr as $row) {
4102            $document = $this->getDocument($row['document']);
4103            /* getting the document can fail if it is outside the root folder
4104             * and checkWithinRootDir is enabled.
4105             */
4106            if($document) {
4107                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum'], $row['fileSize'], $row['checksum']);
4108                $versions[] = $version;
4109            }
4110        }
4111        return $versions;
4112
4113    } /* }}} */
4114
4115    /**
4116     * Returns document content which has no checksum set
4117     *
4118     * This method is for finding document content without a checksum
4119     * set in the database. The checksum of a document content was introduced
4120     * in version 4.0.0 of SeedDMS for finding duplicates.
4121     * @return bool|SeedDMS_Core_Document[]
4122     */
4123    function getNoChecksumDocumentContent() { /* {{{ */
4124        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `checksum` = '' OR `checksum` is null";
4125        $resArr = $this->db->getResultArray($queryStr);
4126        if ($resArr === false)
4127            return false;
4128
4129        /** @var SeedDMS_Core_Document[] $versions */
4130        $versions = array();
4131        foreach($resArr as $row) {
4132            $document = $this->getDocument($row['document']);
4133            /* getting the document can fail if it is outside the root folder
4134             * and checkWithinRootDir is enabled.
4135             */
4136            if($document) {
4137                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
4138                $versions[] = $version;
4139            }
4140        }
4141        return $versions;
4142
4143    } /* }}} */
4144
4145    /**
4146     * Returns document content which has the incorrect file type
4147     *
4148     * This method is for finding document content with an incorrect
4149     * or missing file type. It just checks documents contents
4150     * with a certain mime type.
4151     * @return bool|SeedDMS_Core_Document[]
4152     */
4153    function getWrongFiletypeDocumentContent() { /* {{{ */
4154        $queryStr = "SELECT * FROM `tblDocumentContent` WHERE `mimeType` in ('application/zip', 'application/pdf', 'image/png', 'image/gif', 'image/jpg', 'audio/mp3', 'text/rtf')";
4155        $resArr = $this->db->getResultArray($queryStr);
4156        if ($resArr === false)
4157            return false;
4158
4159        /** @var SeedDMS_Core_Document[] $versions */
4160        $versions = array();
4161        foreach($resArr as $row) {
4162            $expect = '';
4163            switch($row['mimeType']) {
4164            case "application/zip":
4165            case "application/pdf":
4166            case "image/png":
4167            case "image/gif":
4168            case "image/jpg":
4169            case "audio/mp3":
4170            case "text/rtf":
4171                $expect = substr($row['mimeType'], -3, 3);
4172                break;
4173            }
4174            if($expect) {
4175                if($row['fileType'] != '.'.$expect) {
4176                    /** @var SeedDMS_Core_Document $document */
4177                    $document = new $this->classnames['document']($row['document'], '', '', '', '', '', '', '', '', '', '', '');
4178                    $document->setDMS($this);
4179                    $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
4180                    $versions[] = $version;
4181                }
4182            }
4183        }
4184        return $versions;
4185
4186    } /* }}} */
4187
4188    /**
4189     * Returns document content which is duplicated
4190     *
4191     * This method is for finding document content which is available twice
4192     * in the database. The checksum of a document content was introduced
4193     * in version 4.0.0 of SeedDMS for finding duplicates.
4194     * @return array|bool
4195     */
4196    function getDuplicateDocumentContent() { /* {{{ */
4197        $queryStr = "SELECT a.*, b.`id` as dupid FROM `tblDocumentContent` a LEFT JOIN `tblDocumentContent` b ON a.`checksum`=b.`checksum` WHERE a.`id`!=b.`id` ORDER BY a.`id` LIMIT 1000";
4198        $resArr = $this->db->getResultArray($queryStr);
4199        if ($resArr === false)
4200            return false;
4201
4202        /** @var SeedDMS_Core_Document[] $versions */
4203        $versions = array();
4204        foreach($resArr as $row) {
4205            $document = $this->getDocument($row['document']);
4206            /* getting the document can fail if it is outside the root folder
4207             * and checkWithinRootDir is enabled.
4208             */
4209            if($document) {
4210                $version = new $this->classnames['documentcontent']($row['id'], $document, $row['version'], $row['comment'], $row['date'], $row['createdBy'], $row['dir'], $row['orgFileName'], $row['fileType'], $row['mimeType'], $row['fileSize'], $row['checksum']);
4211                if(!isset($versions[$row['dupid']])) {
4212                    $versions[$row['id']]['content'] = $version;
4213                    $versions[$row['id']]['duplicates'] = array();
4214                } else
4215                    $versions[$row['dupid']]['duplicates'][] = $version;
4216            }
4217        }
4218        return $versions;
4219
4220    } /* }}} */
4221
4222    /**
4223     * Returns folders which contain documents with none unique sequence number
4224     *
4225     * This method is for finding folders with documents not having a
4226     * unique sequence number. Those documents cannot propperly be sorted
4227     * by sequence and changing their position is impossible if more than
4228     * two documents with the same sequence number exists, e.g.
4229     * doc 1: 3
4230     * doc 2: 5
4231     * doc 3: 5
4232     * doc 4: 5
4233     * doc 5: 7
4234     * If document 4 was to be moved between doc 1 and 2 it get sequence
4235     * number 4 ((5+3)/2).
4236     * But if document 4 was to be moved between doc 2 and 3 it will again
4237     * have sequence number 5.
4238     *
4239     * @return array|bool
4240     */
4241    function getDuplicateSequenceNo() { /* {{{ */
4242        $queryStr = "SELECT DISTINCT `folder` FROM (SELECT `folder`, `sequence` FROM `tblDocuments` GROUP BY `folder`, `sequence` HAVING count(*) > 1) a";
4243        $resArr = $this->db->getResultArray($queryStr);
4244        if ($resArr === false)
4245            return false;
4246
4247        $folders = array();
4248        foreach($resArr as $row) {
4249            $folder = $this->getFolder($row['folder']);
4250            if($folder)
4251                $folders[] = $folder;
4252        }
4253        return $folders;
4254
4255    } /* }}} */
4256
4257    /**
4258     * Returns documents which have link to themselves
4259     *
4260     * @return array|bool
4261     */
4262    function getLinksToItself() { /* {{{ */
4263        $queryStr = "SELECT * FROM `tblDocumentLinks` WHERE `document`=`target`";
4264        $resArr = $this->db->getResultArray($queryStr);
4265        if ($resArr === false)
4266            return false;
4267
4268        $documents = array();
4269        foreach($resArr as $row) {
4270            $document = $this->getDocument($row['document']);
4271            if($document)
4272                $documents[] = $document;
4273        }
4274        return $documents;
4275
4276    } /* }}} */
4277
4278    /**
4279     * Returns a list of reviews, approvals, receipts, revisions which are not
4280     * linked to a user, group anymore
4281     *
4282     * This method is for finding reviews or approvals whose user
4283     * or group  was deleted and not just removed from the process.
4284     *
4285     * @param string $process
4286     * @param string $usergroup
4287     * @return array
4288     */
4289    function getProcessWithoutUserGroup($process, $usergroup) { /* {{{ */
4290        switch($process) {
4291        case 'review':
4292            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentReviewers`";
4293            break;
4294        case 'approval':
4295            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentApprovers`";
4296            break;
4297        case 'receipt':
4298            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentRecipients`";
4299            break;
4300        case 'revision':
4301            $queryStr = "SELECT a.*, b.`name` FROM `tblDocumentRevisors`";
4302            break;
4303        }
4304        /** @noinspection PhpUndefinedVariableInspection */
4305        $queryStr .= " a LEFT JOIN `tblDocuments` b ON a.`documentID`=b.`id` WHERE";
4306        switch($usergroup) {
4307        case 'user':
4308            $queryStr .= " a.`type`=0 and a.`required` not in (SELECT `id` FROM `tblUsers`) ORDER BY b.`id`";
4309            break;
4310        case 'group':
4311            $queryStr .= " a.`type`=1 and a.`required` not in (SELECT `id` FROM `tblGroups`) ORDER BY b.`id`";
4312            break;
4313        }
4314        return $this->db->getResultArray($queryStr);
4315    } /* }}} */
4316
4317    /**
4318     * Removes all reviews, approvals, receipts, revisions which are not linked
4319     * to a user, group anymore
4320     *
4321     * This method is for removing all reviews or approvals whose user
4322     * or group  was deleted and not just removed from the process.
4323     * If the optional parameter $id is set, only this user/group id is removed.
4324     * @param string $process
4325     * @param string $usergroup
4326     * @param int $id
4327     * @return array
4328     */
4329    function removeProcessWithoutUserGroup($process, $usergroup, $id=0) { /* {{{ */
4330        /* Entries of tblDocumentReviewLog or tblDocumentApproveLog are deleted
4331         * because of CASCADE ON
4332         */
4333        switch($process) {
4334        case 'review':
4335            $queryStr = "DELETE FROM tblDocumentReviewers";
4336            break;
4337        case 'approval':
4338            $queryStr = "DELETE FROM tblDocumentApprovers";
4339            break;
4340        case 'receipt':
4341            $queryStr = "DELETE FROM tblDocumentRecipients";
4342            break;
4343        case 'revision':
4344            $queryStr = "DELETE FROM tblDocumentRevisors";
4345            break;
4346        }
4347        /** @noinspection PhpUndefinedVariableInspection */
4348        $queryStr .= " WHERE";
4349        switch($usergroup) {
4350        case 'user':
4351            $queryStr .= " type=0 AND";
4352            if($id)
4353                $queryStr .= " required=".((int) $id)." AND";
4354            $queryStr .= " required NOT IN (SELECT id FROM tblUsers)";
4355            break;
4356        case 'group':
4357            $queryStr .= " type=1 AND";
4358            if($id)
4359                $queryStr .= " required=".((int) $id)." AND";
4360            $queryStr .= " required NOT IN (SELECT id FROM tblGroups)";
4361            break;
4362        }
4363        return $this->db->getResultArray($queryStr);
4364    } /* }}} */
4365
4366    /**
4367     * Returns statitical information
4368     *
4369     * This method returns all kind of statistical information like
4370     * documents or used space per user, recent activity, etc.
4371     *
4372     * @param string $type type of statistic
4373     * @return array|bool returns false if the sql statement fails, returns an empty
4374     * array if no documents or folder where found, otherwise returns a non empty
4375     * array with statistical data
4376     */
4377    function getStatisticalData($type='') { /* {{{ */
4378        switch($type) {
4379            case 'docsperuser':
4380                $queryStr = "SELECT ".$this->db->concat(array('b.`fullName`', "' ('", 'b.`login`', "')'"))." AS `key`, count(`owner`) AS total FROM `tblDocuments` a LEFT JOIN `tblUsers` b ON a.`owner`=b.`id` GROUP BY `owner`, `key`";
4381                $resArr = $this->db->getResultArray($queryStr);
4382                if(is_bool($resArr) && $resArr == false)
4383                    return false;
4384
4385                return $resArr;
4386            case 'foldersperuser':
4387                $queryStr = "SELECT ".$this->db->concat(array('b.`fullName`', "' ('", 'b.`login`', "')'"))." AS `key`, count(`owner`) AS total FROM `tblFolders` a LEFT JOIN `tblUsers` b ON a.`owner`=b.`id` GROUP BY `owner`, `key`";
4388                $resArr = $this->db->getResultArray($queryStr);
4389                if(is_bool($resArr) && $resArr == false)
4390                    return false;
4391
4392                return $resArr;
4393            case 'docspermimetype':
4394                $queryStr = "SELECT b.`mimeType` AS `key`, count(`mimeType`) AS total FROM `tblDocuments` a LEFT JOIN `tblDocumentContent` b ON a.`id`=b.`document` GROUP BY b.`mimeType`";
4395                $resArr = $this->db->getResultArray($queryStr);
4396                if(is_bool($resArr) && $resArr == false)
4397                    return false;
4398
4399                return $resArr;
4400            case 'docspercategory':
4401                $queryStr = "SELECT b.`name` AS `key`, count(a.`categoryID`) AS total FROM `tblDocumentCategory` a LEFT JOIN `tblCategory` b ON a.`categoryID`=b.id GROUP BY a.`categoryID`, b.`name`";
4402                $resArr = $this->db->getResultArray($queryStr);
4403                if(is_bool($resArr) && $resArr == false)
4404                    return false;
4405
4406                return $resArr;
4407            case 'docsperstatus':
4408                /** @noinspection PhpUnusedLocalVariableInspection */
4409                $queryStr = "SELECT b.`status` AS `key`, count(b.`status`) AS total FROM (SELECT a.id, max(b.version), max(c.`statusLogID`) AS maxlog FROM `tblDocuments` a LEFT JOIN `tblDocumentStatus` b ON a.id=b.`documentID` LEFT JOIN `tblDocumentStatusLog` c ON b.`statusID`=c.`statusID` GROUP BY a.`id`, b.`version` ORDER BY a.`id`, b.`statusID`) a LEFT JOIN `tblDocumentStatusLog` b ON a.`maxlog`=b.`statusLogID` GROUP BY b.`status`";
4410                $queryStr = "SELECT b.`status` AS `key`, count(b.`status`) AS total FROM (SELECT a.`id`, max(c.`statusLogID`) AS maxlog FROM `tblDocuments` a LEFT JOIN `tblDocumentStatus` b ON a.id=b.`documentID` LEFT JOIN `tblDocumentStatusLog` c ON b.`statusID`=c.`statusID` GROUP BY a.`id` ORDER BY a.id) a LEFT JOIN `tblDocumentStatusLog` b ON a.maxlog=b.`statusLogID` GROUP BY b.`status`";
4411                $resArr = $this->db->getResultArray($queryStr);
4412                if(is_bool($resArr) && $resArr == false)
4413                    return false;
4414
4415                return $resArr;
4416            case 'docspermonth':
4417                $queryStr = "SELECT *, count(`key`) AS total FROM (SELECT ".$this->db->getDateExtract("date", '%Y-%m')." AS `key` FROM `tblDocuments`) a GROUP BY `key` ORDER BY `key`";
4418                $resArr = $this->db->getResultArray($queryStr);
4419                if(is_bool($resArr) && $resArr == false)
4420                    return false;
4421
4422                return $resArr;
4423            case 'docsaccumulated':
4424                $queryStr = "SELECT *, count(`key`) AS total FROM (SELECT ".$this->db->getDateExtract("date")." AS `key` FROM `tblDocuments`) a GROUP BY `key` ORDER BY `key`";
4425                $resArr = $this->db->getResultArray($queryStr);
4426                if(is_bool($resArr) && $resArr == false)
4427                    return false;
4428
4429                $sum = 0;
4430                foreach($resArr as &$res) {
4431                    $sum += $res['total'];
4432                    /* auxially variable $key is need because sqlite returns
4433                     * a key '`key`'
4434                     */
4435                    $res['key'] = mktime(12, 0, 0, (int) substr($res['key'], 5, 2), (int) substr($res['key'], 8, 2), (int) substr($res['key'], 0, 4)) * 1000;
4436                    $res['total'] = $sum;
4437                }
4438                return $resArr;
4439            case 'docstotal':
4440                $queryStr = "SELECT count(*) AS total FROM `tblDocuments`";
4441                $resArr = $this->db->getResultArray($queryStr);
4442                if(is_bool($resArr) && $resArr == false)
4443                    return false;
4444                return (int) $resArr[0]['total'];
4445            case 'folderstotal':
4446                $queryStr = "SELECT count(*) AS total FROM `tblFolders`";
4447                $resArr = $this->db->getResultArray($queryStr);
4448                if(is_bool($resArr) && $resArr == false)
4449                    return false;
4450                return (int) $resArr[0]['total'];
4451            case 'userstotal':
4452                $queryStr = "SELECT count(*) AS total FROM `tblUsers`";
4453                $resArr = $this->db->getResultArray($queryStr);
4454                if(is_bool($resArr) && $resArr == false)
4455                    return false;
4456                return (int) $resArr[0]['total'];
4457            case 'sizeperuser':
4458                $queryStr = "SELECT ".$this->db->concat(array('c.`fullName`', "' ('", 'c.`login`', "')'"))." AS `key`, sum(`fileSize`) AS total FROM `tblDocuments` a LEFT JOIN `tblDocumentContent` b ON a.id=b.`document` LEFT JOIN `tblUsers` c ON a.`owner`=c.`id` GROUP BY a.`owner`, `key`";
4459                $resArr = $this->db->getResultArray($queryStr);
4460                if(is_bool($resArr) && $resArr == false)
4461                    return false;
4462
4463                return $resArr;
4464            case 'sizepermonth':
4465                $queryStr = "SELECT *, sum(`fileSize`) AS total FROM (SELECT ".$this->db->getDateExtract("date", '%Y-%m')." AS `key`, `fileSize` FROM `tblDocumentContent`) a GROUP BY `key` ORDER BY `key`";
4466                $resArr = $this->db->getResultArray($queryStr);
4467                if(is_bool($resArr) && $resArr == false)
4468                    return false;
4469
4470                return $resArr;
4471            default:
4472                return array();
4473        }
4474    } /* }}} */
4475
4476    /**
4477     * Returns changes with a period of time
4478     *
4479     * This method returns a list of all changes happened in the database
4480     * within a given period of time. It currently just checks for
4481     * entries in the database tables tblDocumentContent, tblDocumentFiles,
4482     * and tblDocumentStatusLog
4483     *
4484     * @param string $startts
4485     * @param string $endts
4486     * @return array|bool
4487     * @internal param string $start start date, defaults to start of current day
4488     * @internal param string $end end date, defaults to end of start day
4489     */
4490    function getTimeline($startts='', $endts='') { /* {{{ */
4491        if(!$startts)
4492            $startts = mktime(0, 0, 0);
4493        if(!$endts)
4494            $endts = $startts+86400;
4495
4496        /** @var SeedDMS_Core_Document[] $timeline */
4497        $timeline = array();
4498
4499        if(0) {
4500        $queryStr = "SELECT DISTINCT `document` FROM `tblDocumentContent` WHERE `date` > ".$startts." AND `date` < ".$endts." OR `revisiondate` > '".date('Y-m-d H:i:s', $startts)."' AND `revisiondate` < '".date('Y-m-d H:i:s', $endts)."' UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts;
4501        } else {
4502        $startdate = date('Y-m-d H:i:s', $startts);
4503        $enddate = date('Y-m-d H:i:s', $endts);
4504        $queryStr = "SELECT DISTINCT `documentID` AS `document` FROM `tblDocumentStatus` LEFT JOIN `tblDocumentStatusLog` ON `tblDocumentStatus`.`statusID`=`tblDocumentStatusLog`.`statusID` WHERE `date` > ".$this->db->qstr($startdate)." AND `date` < ".$this->db->qstr($enddate)." UNION SELECT DISTINCT document FROM `tblDocumentFiles` WHERE `date` > ".$this->db->qstr($startdate)." AND `date` < ".$this->db->qstr($enddate)." UNION SELECT DISTINCT `document` FROM `tblDocumentFiles` WHERE `date` > ".$startts." AND `date` < ".$endts;
4505        }
4506        $resArr = $this->db->getResultArray($queryStr);
4507        if ($resArr === false)
4508            return false;
4509        foreach($resArr as $rec) {
4510            $document = $this->getDocument($rec['document']);
4511            $timeline = array_merge($timeline, $document->getTimeline());
4512        }
4513        return $timeline;
4514
4515    } /* }}} */
4516
4517    /**
4518     * Returns changes with a period of time
4519     *
4520     * This method is similar to getTimeline() but returns more dedicated lists
4521     * of documents or folders which has change in various ways.
4522     *
4523     * @param string $mode
4524     * @param string $startts
4525     * @param string $endts
4526     * @return array|bool
4527     * @internal param string $start start date, defaults to start of current day
4528     * @internal param string $end end date, defaults to end of start day
4529     */
4530    function getLatestChanges($mode, $startts='', $endts='') { /* {{{ */
4531        if(!$startts)
4532            $startts = mktime(0, 0, 0);
4533        if(!$endts)
4534            $endts = $startts+86400;
4535
4536        $startdate = date('Y-m-d H:i:s', $startts);
4537        $enddate = date('Y-m-d H:i:s', $endts);
4538
4539        $objects = [];
4540        switch($mode) {
4541        case 'statuschange':
4542            /* Count entries in tblDocumentStatusLog for each tblDocumentStatus and
4543             * take only those into account with at least 2 log entries. For the
4544             * document id do a left join with tblDocumentStatus
4545             * This is similar to ttstatid + the count + the join
4546             * c > 1 is required to find only those documents with a changed status
4547             * This sql statement appears to be much to complicated.
4548             */
4549            //$queryStr = "SELECT `a`.*, `tblDocumentStatus`.`documentID` as `document` FROM (SELECT `tblDocumentStatusLog`.`statusID` AS `statusID`, MAX(`tblDocumentStatusLog`.`statusLogID`) AS `maxLogID`, COUNT(`tblDocumentStatusLog`.`statusLogID`) AS `c`, `tblDocumentStatusLog`.`date` FROM `tblDocumentStatusLog` GROUP BY `tblDocumentStatusLog`.`statusID` HAVING `c` > 1 ORDER BY `tblDocumentStatusLog`.`date` DESC) `a` LEFT JOIN `tblDocumentStatus` ON `a`.`statusID`=`tblDocumentStatus`.`statusID` WHERE `a`.`date` > ".$this->db->qstr($startdate)." AND `a`.`date` < ".$this->db->qstr($enddate)." ";
4550            $queryStr = "SELECT DISTINCT `tblDocumentStatus`.`documentID` as    `document` FROM `tblDocumentStatusLog` LEFT JOIN `tblDocumentStatus` ON `tblDocumentStatusLog`.`statusID` = `tblDocumentStatus`.`statusID` WHERE `tblDocumentStatusLog`.`date` > ".$this->db->qstr($startdate)." AND `tblDocumentStatusLog`.`date` < ".$this->db->qstr($enddate)." ORDER BY `tblDocumentStatusLog`.`date` DESC";
4551            $resArr = $this->db->getResultArray($queryStr);
4552            if ($resArr === false)
4553                return false;
4554            foreach($resArr as $rec) {
4555                if($object = $this->getDocument($rec['document']))
4556                    $objects[] = $object;
4557            }
4558            break;
4559        case 'newdocuments':
4560            $queryStr = "SELECT `id` AS `document` FROM `tblDocuments` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
4561            $resArr = $this->db->getResultArray($queryStr);
4562            if ($resArr === false)
4563                return false;
4564            foreach($resArr as $rec) {
4565                if($object = $this->getDocument($rec['document']))
4566                    $objects[] = $object;
4567            }
4568            break;
4569        case 'updateddocuments':
4570            /* DISTINCT is need if there is more than 1 update of the document in the
4571             * given period of time. Without it, the query will return the document
4572             * more than once.
4573             */
4574            $queryStr = "SELECT DISTINCT `document` AS `document` FROM `tblDocumentContent` LEFT JOIN `tblDocuments` ON `tblDocumentContent`.`document`=`tblDocuments`.`id` WHERE `tblDocumentContent`.`date` > ".$startts." AND `tblDocumentContent`.`date` < ".$endts." AND `tblDocumentContent`.`date` > `tblDocuments`.`date` ORDER BY `tblDocumentContent`.`date` DESC";
4575            $resArr = $this->db->getResultArray($queryStr);
4576            if ($resArr === false)
4577                return false;
4578            foreach($resArr as $rec) {
4579                if($object = $this->getDocument($rec['document']))
4580                    $objects[] = $object;
4581            }
4582            break;
4583        case 'newfolders':
4584            $queryStr = "SELECT `id` AS `folder` FROM `tblFolders` WHERE `date` > ".$startts." AND `date` < ".$endts." ORDER BY `date` DESC";
4585            $resArr = $this->db->getResultArray($queryStr);
4586            if ($resArr === false)
4587                return false;
4588            foreach($resArr as $rec) {
4589                if($object = $this->getFolder($rec['folder']))
4590                    $objects[] = $object;
4591            }
4592            break;
4593        }
4594        return $objects;
4595    } /* }}} */
4596
4597    /**
4598     * Set a callback function
4599     *
4600     * The function passed in $func must be a callable and $name must not be empty.
4601     *
4602     * Setting a callback with this method will remove all previously
4603     * set callbacks. Use {@see SeedDMS_Core_DMS::addCallback()} to register
4604     * additional callbacks.
4605     * This method does not check if there is a callback with the given name.
4606     *
4607     * @param string $name internal name of callback
4608     * @param mixed $func function name as expected by {call_user_method}
4609     * @param mixed $params parameter passed as the first argument to the
4610     *        callback
4611     * @return bool true if adding the callback succeeds otherwise false
4612     */
4613    function setCallback($name, $func, $params=null) { /* {{{ */
4614        if($name && $func && is_callable($func)) {
4615            $this->callbacks[$name] = array(array($func, $params));
4616            return true;
4617        } else {
4618            return false;
4619        }
4620    } /* }}} */
4621
4622    /**
4623     * Add a callback function
4624     *
4625     * The function passed in $func must be a callable and $name must not be empty.
4626     * This method does not check if there is a callback with the given name.
4627     *
4628     * @param string $name internal name of callback
4629     * @param mixed $func function name as expected by {call_user_method}
4630     * @param mixed $params parameter passed as the first argument to the
4631     *        callback
4632     * @return bool true if adding the callback succeeds otherwise false
4633     */
4634    function addCallback($name, $func, $params=null) { /* {{{ */
4635        if($name && $func && is_callable($func)) {
4636            $this->callbacks[$name][] = array($func, $params);
4637            return true;
4638        } else {
4639            return false;
4640        }
4641    } /* }}} */
4642
4643    /**
4644     * Check if a callback with the given name has been set
4645     *
4646     * @param string $name internal name of callback
4647     * @return bool true if callback exists otherwise false
4648     */
4649    function hasCallback($name) { /* {{{ */
4650        if($name && !empty($this->callbacks[$name]))
4651            return true;
4652        return false;
4653    } /* }}} */
4654
4655}